I am trying to implement a partial swipe in RecyclerView and add a delete button. I would like to delete the swiped row only after the user clicks the delete button. I am using ItemTouchHelper. SimpleCallback and was able to achieve the partial swipe with the code below. I have two pending tasks:
I am implementing partial swipe using the onChildDraw method. I am currently drawing a red rectangle on partial swipe. I would want to add the text "Delete" inside it. Is it possible to add a Button instead of drawing a rectangle?
How do I add the click listener on the rectange/button to perform the delete action.
#Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
try {
Bitmap icon;
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
View itemView = viewHolder.itemView;
float height = (float) itemView.getBottom() - (float) itemView.getTop();
float width = height / 5;
viewHolder.itemView.setTranslationX(dX / 5);
Paint paint = new Paint();
paint.setColor(Color.parseColor("#D32F2F"));
RectF background = new RectF((float) itemView.getRight() + dX / 5, (float) itemView.getTop(), (float) itemView.getRight(), (float) itemView.getBottom());
RectF icon_dest = new RectF((float) (itemView.getRight() + dX /7), (float) itemView.getTop()+width, (float) itemView.getRight()+dX/20, (float) itemView.getBottom()-width);
c.drawBitmap(null, null, icon_dest, paint);
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
} catch (Exception e) {
e.printStackTrace();
}
}
To answer your questions:
Yes. It's possible, but not in onChildDraw method. There are a lot
3rd party libraries use a two-layer approach. The first layer is
your item view, the second has all the buttons. When you swipe, it
reveals the button layer, and you can handle click event by setting
buttons' OnClickListener.
To add OnClickListener to the buttons you drawn in onChildDraw
method, you will have to set recylerView.setOnTouchListener to get
(x,y) on screen. Please check AdamWei's post here for
instruction.
You can also check my post here. I have implemented the desired effect by using ItemTouchHelper.SimpleCallback. It's a simple helper class, easy to use.
Related
I have a horizontal RecyclerView and am attempting to programatically scroll by an x value.
This has so far been achieved with smoothScrollBy(x, y), however, I can't for the life of me find a solution where I can set a scroll duration, e.g. 1000ms.
Any help would me much appreciated, thanks.
The code is as follows:
private void focus() {
View focusedRecyclerViewItem = getFocusedRecyclerViewItem();
TextView focusedTextView = getFocusedTextView(focusedRecyclerViewItem);
focusedTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 64);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
mRecyclerView.clearOnScrollListeners();
countdown();
}
});
int x = (int) focusedRecyclerViewItem.getX() - mRecyclerView.getWidth() / 2;
mRecyclerView.smoothScrollBy(x, 0);
}
To clarify as the question was not initially clear - what I am looking for is a custom duration for the smoothScrollBy() method when it is called, not a duration before the smoothScrollBy() method is called.
I have searched for a possible solution to imitating the collapsible toolbar but I can't seem to start it with my current knowledge on handling complex layouts.
From the layout above, I have a docked layout above, an imageview, 2 textviews. What I want to do is when I scroll up, the imageview will transfer its self at the top right, the textview without border will be pushed at the top, and the last textview should disappear.
Just like this.
As of now, I was able to move the ImageView to the required position when scrolling with this code:
private void scaleImage(float x, float y) {
movingView.setScaleX(x);
movingView.setScaleY(y);
movingView.setPivotX(1.3f * movingView.getWidth());
movingView.setPivotY(1.5f * movingView.getHeight());
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
float offsetFactorDown = (float) (getScrollY()) / (float) scrollRange;
float scaleFactor = 1F - offsetFactorDown * 0.9F;
scaleImage(scaleFactor, scaleFactor);
}
The getScrollY() is not consistent which only gives me the right position when I am scrolling slowly. What else can I use to be the basis for the offset which is consistent?
This question already has answers here:
RecyclerView ItemTouchHelper Buttons on Swipe
(12 answers)
Closed 3 years ago.
I have created RecyclerView which contains CardView in order to show data. I would like to implement iOS style of swiping list elements to show action buttons.
My method which should allow me to show icon after swiping left an RecyclerView item:
public void initializeListeners() {
ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
if (direction == ItemTouchHelper.LEFT) {
Toast.makeText(getView().getContext(),"LEFT",Toast.LENGTH_LONG).show();
}
}
#Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
Bitmap icon;
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
View itemView = viewHolder.itemView;
float height = (float) itemView.getBottom() - (float) itemView.getTop();
float width = height / 3;
if (dX < 0) {
p.setColor(Color.parseColor("#D32F2F"));
RectF background = new RectF((float) itemView.getRight() + dX/4, (float) itemView.getTop(), (float) itemView.getRight(), (float) itemView.getBottom());
c.drawRect(background, p);
icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_delete_black_24dp);
RectF icon_dest = new RectF((float) itemView.getRight() - 2 * width, (float) itemView.getTop() + width, (float) itemView.getRight() - width, (float) itemView.getBottom() - width);
c.drawBitmap(icon, null, icon_dest, p);
}
}
super.onChildDraw(c, recyclerView, viewHolder, dX/4, dY, actionState, isCurrentlyActive);
}
};
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);
itemTouchHelper.attachToRecyclerView(binding.myPlans);
}
effect of this is:
I would like to make this icon clickable in order to send HTTP request using id of object of clicked possition in RecyclerView (after alertview confirmation)
is it possible? I was trying to replace Bitmap for ImageButton with no success
If you consider Swipeable items in lists, the logic is a bit different for Android and iOS. In Android you don't need to confirm deletion with a click. The fact that user swiped the item is enough of a confirmation.
That's why ItemTouchHelper won't give you a way to attach an OnClickListener.
You have two choices:
You can write your own custom swipe management system (painful).
Agree on Android way of doing that and ask user for confirmation after the swipe.
I wrote a blog post describing the steps needed for implementing this kind of feature.
Add the following class to your project: SwipeRevealLayout.java
Adjust your layout code for your RecyclerView ViewHolder and add the SwipeRevealLayout component as the container for both the top and bottom layer of your RecyclerView Item. For an example of how to set it up: list_item_main.xml
Ensure the bottom layer is the first layout component within the SwipeRevealLayout container.
Make sure to use ‘wrap_content’ or a predefined width for your bottom layer. I tested out using ‘match_parent’ and the top layer did a good magic trick and disappeared.
If you are adding a clickable function on the bottom layer, ensure the top layer has android:clickable=”true” otherwise clicks for the bottom layer components will still trigger when you click on the top layer.
Optional: You can define what edge you want to drag from. By default, it will drag from the left, in the example project I defined it to drag from the right. Specify it with app:dragFromEdge=”{edge to drag from}" when specifying the attributes for the SwipeRevealLayout component.
If you're interested in viewing the full blog post, check it out here: https://android.jlelse.eu/android-recyclerview-swipeable-items-46a3c763498d
The background drawn is a Canvas, Canvas don't allow to implements clicks.
I have found a way to simulate the click on your trash view within the onInterceptTouchEvent of RecyclerView.OnItemTouchListener.
First, in your onBindViewHolder you have to set the tag of your view as it viewHolder : viewHolder.itemView.setTag(viewHolder).
Then :
#Override
public void onInterceptTouchEvent (RecyclerView rv, MotionEvent e) {
View viewSwipedRight = rv.findChildViewUnder(e.getX() - rv.getWidth(), e.getY());
if (viewSwipedRight != null && e.getAction() == MotionEvent.ACTION_DOWN) {
ViewHolder viewHolder = (ViewHolder) viewSwipedRight.getTag();
if (e.getX() >= viewHolder.trashIcon.getX()) {
// Your icon is clicked !
}
}
}
Explanation :
When swiped left, your view is still here, but not on screen because it has moved at the left of the recyclerview, so View viewSwipedRight = rv.findChildViewUnder(e.getX() - rv.getWidth(), e.getY()); will find the view at your Y click at the left of the recyclerView on screen (thanks to e.getX() - rv.getWidth()).
Then you look if the event X matches with your trash icon's X within its own view with e.getX() >= viewHolder.trashIcon.getX() (your trashIcon has to be in your viewHolder).
If both of those conditions match, you have clicked your trash icon.
I've got a remove on swipe, that draws a background (much like the Inbox app), implemented by an ItemTouchHelper - by overriding the onChilDraw method and drawing a rectangle on the provided canvas:
ItemTouchHelper mIth = new ItemTouchHelper(
new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
remove(viewHolder.getAdapterPosition());
}
public boolean onMove(RecyclerView recyclerview, RecyclerView.ViewHolder v, RecyclerView.ViewHolder target) {
return false;
}
#Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
View itemView = viewHolder.itemView;
Drawable d = ContextCompat.getDrawable(context, R.drawable.bg_swipe_item_right);
d.setBounds(itemView.getLeft(), itemView.getTop(), (int) dX, itemView.getBottom());
d.draw(c);
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
});
The remove method called above is in the Adapter:
public void remove(int position) {
items.remove(position);
notifyItemRemoved(position);
}
The background draws out nicely, but when notifyItemRemoved is called (according to Mr. Debugger), the RecyclerView first deletes my pretty green background, and then pushes the two adjacent items together.
I would like it to keep the background there while it does that (just like the Inbox app). Is there any way to do that?
I had the same issue and I didn't wanna introduce a new lib just to fix it. The RecyclerView is not deleting your pretty green background, it's just redrawing itself, and your ItemTouchHelper is not drawing anymore. Actually it's drawing but the dX is 0 and is drawing from the itemView.getLeft() (which is 0) to dX (which is 0) so you see nothing. And it's drawing too much, but I'll come back to it later.
Anyway back to the background while rows animate: I couldn't do it within ItemTouchHelper and onChildDraw. In the end I had to add another item decorator to do it. It goes along these lines:
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getItemAnimator().isRunning()) {
// find first child with translationY > 0
// draw from it's top to translationY whatever you want
int top = 0;
int bottom = 0;
int childCount = parent.getLayoutManager().getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getLayoutManager().getChildAt(i);
if (child.getTranslationY() != 0) {
top = child.getTop();
bottom = top + (int) child.getTranslationY();
break;
}
}
// draw whatever you want
super.onDraw(c, parent, state);
}
}
This code takes into account only rows animating up, but you should also consider rows coming down. That happens if you swipe delete the last row, rows above are gonna animate down to that space.
When I said your ItemTouchHelper is drawing too much what I meant was: Looks like ItemTouchHelper keeps ViewHolders of removed rows in case they need to be restored. It's also calling onChildDraw for those VHs in addition to the VH being swiped. Not sure about memory management implications of this behavior but I needed an additional check in the start of onChildDraw to avoid drawing for "fantom" rows.
if (viewHolder.getAdapterPosition() == -1) {
return;
}
In your case it's drawing from left=0 to right=0 so you don't see anything but the overhead is there. If you start seeing previously swiped away rows drawing their backgrounds that is the reason.
EDIT: I had a go at this, see this blog post and this github repo.
I managed to get it to work by using Wasabeefs's recyclerview-animators library.
My ViewHolder now extends the library's provided AnimateViewHolder:
class MyViewHolder extends AnimateViewHolder {
TextView textView;
public MyViewHolder(View itemView) {
super(itemView);
this.textView = (TextView) itemView.findViewById(R.id.item_name);
}
#Override
public void animateAddImpl(ViewPropertyAnimatorListener listener) {
ViewCompat.animate(itemView)
.translationY(0)
.alpha(1)
.setDuration(300)
.setListener(listener)
.start();
}
#Override
public void preAnimateAddImpl() {
ViewCompat.setTranslationY(itemView, -itemView.getHeight() * 0.3f);
ViewCompat.setAlpha(itemView, 0);
}
#Override
public void animateRemoveImpl(ViewPropertyAnimatorListener listener) {
ViewCompat.animate(itemView)
.translationY(0)
.alpha(1)
.setDuration(300)
.setListener(listener)
.start();
}
}
The overrided function implementations are identical to what is in recyclerview-animators' readme on github.
It also seems necessary to change the ItemAnimator to a custom one and set the removeDuration to 0 (or another low value - this is to prevent some flickering):
recyclerView.setItemAnimator(new SlideInLeftAnimator());
recyclerView.getItemAnimator().setRemoveDuration(0);
This doesn't cause any problems as the normal (non-swiping) remove animation used is the one in the AnimateViewHolder.
All other code was kept the same as in the question. I haven't had the time to figure out the inner workings of this yet, but if anyone feels like doing it feel free to update this answer.
Update: Setting recyclerView.getItemAnimator().setRemoveDuration(0); actually breaks the "rebind" animation of the swipe. Fortunately, removing that line and setting a longer duration in animateRemoveImpl (500 works for me) also solves the flickering problem.
Update 2: Turns out that ItemTouchHelper.SimpleCallback uses ItemAnimator's animation durations, which is why the above setRemoveDuration(0) breaks the swipe animation. Simply overriding it's method getAnimationDuration to:
#Override
public long getAnimationDuration(RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {
return animationType == ItemTouchHelper.ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION
: DEFAULT_SWIPE_ANIMATION_DURATION;
}
solves that problem.
Just update the adapter position and then remove the animation
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
if (direction == ItemTouchHelper.LEFT) {
remove(position);
} else {
mAdapter.notifyItemChanged(position);
}
}
I have a situation in that I am using a horizontal scroll view with images and using buttons to smooth scroll to the different image locations. Now it works okay I was just wondering if anyone knew of anyway to slow down the smooth scroll method, i.e. having a longer annimation time? As currently the snapping happens pretty quickly.
Perhaps through an override of the smoothscroll, I have tried to search for this/examples but to no luck.
So any ideas?
Thanks,
Si
How About:
ObjectAnimator animator=ObjectAnimator.ofInt(yourHorizontalScrollView, "scrollX",targetXScroll );
animator.setDuration(800);
animator.start();
THis is one way, which works well for me:
new CountDownTimer(2000, 20) {
public void onTick(long millisUntilFinished) {
hv.scrollTo((int) (2000 - millisUntilFinished), 0);
}
public void onFinish() {
}
}.start();
So here the horizontal scroll view (hv) moves in two seconds from position 0 to 2000 or to the end of the view if smaller than 2000px. Easy to adjust...
Subclass HorizontalScrollView, use reflection to get access to the private field mScroller in HorizontalScrollView. Of course, this will break if the underlying class changes the field name, it defaults back to original scroll implemenation.
The call myScroller.startScroll(scrollX, getScrollY(), dx, 0, 500); changes the scroll speed.
private OverScroller myScroller;
private void init()
{
try
{
Class parent = this.getClass();
do
{
parent = parent.getSuperclass();
} while (!parent.getName().equals("android.widget.HorizontalScrollView"));
Log.i("Scroller", "class: " + parent.getName());
Field field = parent.getDeclaredField("mScroller");
field.setAccessible(true);
myScroller = (OverScroller) field.get(this);
} catch (NoSuchFieldException e)
{
e.printStackTrace();
} catch (IllegalArgumentException e)
{
e.printStackTrace();
} catch (IllegalAccessException e)
{
e.printStackTrace();
}
}
public void customSmoothScrollBy(int dx, int dy)
{
if (myScroller == null)
{
smoothScrollBy(dx, dy);
return;
}
if (getChildCount() == 0)
return;
final int width = getWidth() - getPaddingRight() - getPaddingLeft();
final int right = getChildAt(0).getWidth();
final int maxX = Math.max(0, right - width);
final int scrollX = getScrollX();
dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
myScroller.startScroll(scrollX, getScrollY(), dx, 0, 500);
invalidate();
}
public void customSmoothScrollTo(int x, int y)
{
customSmoothScrollBy(x - getScrollX(), y - getScrollY());
}
Its a scroller the scroll automatically and continously. It was made to show a credits screen by continously scrolling through a list of images. This might help you or give you some idea.
https://github.com/blessenm/SlideshowDemo
Use .smoothScrollToPositionFromTop instead. Example
listView.smoothScrollToPositionFromTop(scroll.pos(),0,scroll.delay());
wherescroll is a simple variable from a class that takes current screen position .get() returns new position .pos() and time of smooth scrolling .delay ... etc
Or even easier, .smoothScrollTo(). Example:
hsv.smoothScrollTo(x, y);
Docs: Android Developer ScrollView docs
Have a look at http://developer.android.com/reference/android/widget/Scroller.html:
The duration of the scroll can be passed in the constructor and specifies the maximum time that the scrolling animation should take