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.
Related
I am new to android and hence RV and I am trying to achieve the layout where the first and last card are not centered and instead show more of the cards after and before them. Maybe at In this case I can see 16dp for the second cards and same thing for the penultimate card which makes the first and last card not centered.
But 8dp each for the rest of the cards so the intermediate cards appear centered. Maybe using itemDecoration somehow for the 2nd and the penultimate card somehow.
I was able to achieve showing parts of next and prev cards by following what is suggested here, but that only centers all the cards uniformly :
How to show part of next/previous card RecyclerView
I tried overriding getItemOffsets but it gets triggered everytime I scroll to the first or the last card and moves the 2nd and 2nd to last card incorrectly
and also doesn't center them correctly when I scroll to them.
public static class MyItemDecoration extends RecyclerView.ItemDecoration {
#Override
public void getItemOffsets(#NonNull Rect outRect, #NonNull View view, #NonNull RecyclerView parent, #NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
final int itemPosition = parent.getChildAdapterPosition(view);
if (itemPosition == RecyclerView.NO_POSITION) {
return;
}
final int itemCount = state.getItemCount();
if (itemCount > 0 && itemPosition == 1) {
outRect.left -= 16;
outRect.right -= 16;
}
else if (itemCount > 0 && itemPosition == itemCount - 1) {
outRect.left += 16;
outRect.right += 16;
}
}
}
RV Setup
SnapHelper snapHelper = new PagerSnapHelper();
RecyclerView rv = getBinding().rv;
rv.setOnFlingListener(null);
snapHelper.attachToRecyclerView(rv);
PagerSnapHelper centers the RecyclerView items including the decorations, so, unless the decoration widths are balanced, they won't always be centered. This may be what you are seeing.
Try the following for the decoration. This code applies the full-width decoration to the start of the first item and the end of the last item; otherwise, a half decoration width is used. By setting up the decorations this way, you are centering items that have balanced left and right decorations.
DividerItemDecoration decoration =
new DividerItemDecoration(getApplicationContext(), HORIZONTAL) {
private int mDecorationWidth = (int) (getResources().getDisplayMetrics().density * 8);
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
final int pos = parent.getChildAdapterPosition(view);
if (pos == RecyclerView.NO_POSITION) {
return;
}
if (pos == 0) {
outRect.set(mDecorationWidth, 0, mDecorationWidth / 2, 0);
} else if (pos == parent.getAdapter().getItemCount() - 1) {
outRect.set(mDecorationWidth / 2, 0, mDecorationWidth, 0);
} else {
outRect.set(mDecorationWidth / 2, 0, mDecorationWidth / 2, 0);
}
}
};
Here is a video showing the results with gray vertical dividers.
If you already have the decorations working to your satisfaction, you can override calculateDistanceToFinalSnap() in PagerSnapHelper to center all views except the first and last view as follows. See calculatedistancetofinalsnap(). Once the PageSnapHelper identifies a target view to snap to, calculatedistancetofinalsnap() is called to determine how many pixels to move to perform the snap. Here, we are moving just enough pixels to center the view (without decorations) in the RecyclerView. PageSnapHelper does the right thing for the first and last items, so we just call the super for these.
PagerSnapHelper pagerSnapHelper = new PagerSnapHelper() {
#Override
public int[] calculateDistanceToFinalSnap(#NonNull RecyclerView.LayoutManager layoutManager,
#NonNull View targetView) {
LinearLayoutManager lm = (LinearLayoutManager) layoutManager;
int pos = mRecycler.getChildAdapterPosition(targetView);
// If first or last view, the default implementation works.
if (pos == 0 || pos == lm.getItemCount() - 1) {
return super.calculateDistanceToFinalSnap(layoutManager, targetView);
}
// Force centering in the view without its decorations.
// targetCenter is the location of the center of the view we want to center.
int targetCenter = targetView.getLeft() + targetView.getWidth() / 2;
// Distance is the number of pixels to move the target so that its center
// lines up with the center of the RecyclerView (mRecycler.getWidth() / 2)
int distance = targetCenter - mRecycler.getWidth() / 2;
return new int[]{distance, 0};
}
};
Either way will work.
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.
Iphone App video link
How I can design and develop view which is posted in above video? This is basically the item expansion of recyclerview with animation. I have tried with onItemtouchlistener of recyclerview and also with some custom view with animation, but didn't get the accurate result.
Finally i came accross addonscrolllistener, this give me results but not accurate.
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if(newState == RecyclerView.FOCUS_UP) {
System.out.println("hello, ia m going up");
}
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0){
TextView tv = (TextView)recyclerView.findViewById(R.id.title);
//tv.setVisibility(View.VISIBLE);
if (tv.getVisibility()==View.VISIBLE){
System.out.println("yes");
}else {
slideToTop(tv);
}
}
}
});
private void slideToTop(View view){
TranslateAnimation animate = new TranslateAnimation(0,0,0,-view.getHeight());
animate.setDuration(1000);
animate.setFillAfter(false);
view.startAnimation(animate);
view.setVisibility(View.VISIBLE);
}
I think your question is too broad - however, here is some psuedocode:
onScrolled() {
View child = getSecondVisibleChild();
int distanceFromTop = child.getTop();
int distanceAtWhichExpandingShouldOccur = 100;
if(distanceFromTop < distanceAtWhichExpandingShouldOccur ) {
child.setHeight(child.getOriginalHeight() + (distanceFromTop - distanceAtWhichExpandingShouldOccur))
}
}
So you'll notice, the second visible child is the one who's height changes. It changed when it's less than distanceAtWhichExpandingShouldOccur from the top of the window. Its height changes to ensure its bottom remains stationary - therefore its height is increasing at the same pace its top is moving.
Once it's no longer the second visible child (aka, its top is 0), it should be scrolled off as normal and the next child should have its height changed when its top is less than distanceAtWhichExpandingShouldOccur.
This library can be a study case for you: https://github.com/florent37/MaterialLeanBack
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);
}
}
Actually I don't know how it properly called - overlay, parallax or slideUP, whatever, I have an Activity called "cafe details" which presents a Container(LinearLayout) with header information (name, min price, delivery time min e.t.c) and other container (ViewPager) which contains a ExpandableListView with something information (menus&dishes) and all I want to do is slide up my Viewpager when scrolls listview to scpecific Y position to cover(or overlay) header information.
A similar effect (but with parallax that I don't need to use) looks like this
I can detect when user scrolling listview down or up but how I can move container with ViewPager to overlay other container? Please give me ideas, regards.
UPD
I have tried a huge number of ways how to implement it and all of them unfortunately are not suitable. So now I have come to next variant - add scroll listener to ListView, calculate scrollY position of view and then based on that move the viewpager on y axis by calling setTranslationY();
Here is some code
1) ViewPager's fragment
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView absListView, int i) {
}
#Override
public void onScroll(AbsListView absListView, int i, int i1, int i2) {
if (getActivity() != null) {
((MainActivity) getActivity()).resizePagerContainer(absListView);
}
}
});
2) MainActivity
//object fields
int previousPos;
private float minTranslation;
private float minHeight;
<--------somewhere in onCreate
minTranslation = - (llVendorDescHeaders.getMeasuredHeight()+llVendorDescNote.getMeasuredHeight());
//llVendorDescHeaders is linearLayout with headers that should be hidden
//lVendorDescNote is a textView on green background;
minHeight = llVendorDescriptionPagerContainer.getMeasuredHeight();
//llVendorDescriptionPagerContainer is a container which contains ViewPager
--------->
public void resizePagerContainer(AbsListView absListView){
final int scrollY = getScrollY(absListView);
if (scrollY != previousPos) {
final float translationY = Math.max(-scrollY, minTranslation);
llVendorDescriptionPagerContainer.setTranslationY(translationY);
previousPos = scrollY;
}
}
private int getScrollY(AbsListView view) {
View child = view.getChildAt(0);
if (child == null) {
return 0;
}
int firstVisiblePosition = view.getFirstVisiblePosition();
int top = child.getTop();
return -top + firstVisiblePosition * child.getHeight() ;
}
This simple solution unfortunately has a problem - it is blinking and twitching (I don't know how to call it right) when scrolls slowly. So instead setTranslationY() I've used an objectAnimator:
public void resizePagerContainer(AbsListView absListView){
.............
ObjectAnimator moveAnim = ObjectAnimator.ofFloat(llVendorDescriptionPagerContainer, "translationY", translationY);
moveAnim.start();
..............
}
I don't like this solution because 1) anyway it does resize viewpager with delay, not instantly 2) I don't think that is good idea to create many ObjectAnimator's objects every time when I scroll my listView.
Need your help and fresh ideas. Regards.
I'm assuming that you are scrolling the top header (the ImageView is a child of the header) based on the scrollY of the ListView/ScrollView, as shown below:
float translationY = Math.max(-scrollY, mMinHeaderTranslation);
mHeader.setTranslationY(translationY);
mTopImage.setTranslationY(-translationY / 3); // For parallax effect
If you want to stick the header/image to a certain dimension and continue the scrolling without moving it anymore, then you can change the value of mMinHeaderTranslation to achieve that effect.
//change this value to increase the dimension of header stuck on the top
int tabHeight = getResources().getDimensionPixelSize(R.dimen.tab_height);
mHeaderHeight = getResources().getDimensionPixelSize(R.dimen.header_height);
mMinHeaderTranslation = -mHeaderHeight + tabHeight;
The code snippets above are in reference to my demo but I think it's still general enough for you.
If you're interested you can check out my demo
https://github.com/boxme/ParallaxHeaderViewPager
Have you tried CoordinatorLayout from this new android's design support library? It looks like it's what you need. Check this video from 3:40 or this blog post.