ListView item scroll animation ("UIKit Dynamics" -like)
Asked Answered
M

7

64

I am attempting to animate the ListView items when a scroll takes place. More specifically, I am trying to emulate the scroll animations from the iMessage app on iOS 7. I found a similar example online:

To clarify, I'm trying to achieve the "fluid" movement effect on the items when the user scrolls, not the animation when a new item is added. I've attempted to modify the Views in my BaseAdapter and I've looked into the AbsListView source to see if I could somehow attach an AccelerateInterpolator somewhere that would adjust the draw coordinates sent to the children Views (if that is even how AbsListView is designed). I've been unable to make any progress so far.

Does anybody have any ideas of how to replicate this behaviour?


For the record to help with googling: this is called "UIKit Dynamics" on ios.

How to replicate Messages bouncing bubbles in iOS 7

It is built-in to recent iOS releases. However it's still somewhat hard to use. (2014) This is the post on it everyone copies:widely copied article Surprisingly, UIKit Dynamics is only available on apple's "collection view", not on apple's "table view" so all the iOS debs are having to convert stuff from table view to "collection view"

The library everyone is using as a starting point is BPXLFlowLayout, since that person pretty much cracked copying the feel of the iphone text messages app. In fact, if you were porting it to Android I guess you could use the parameters in there to get the same feel. FYI I noticed in my android fone collection, HTC phones have this effect, on their UI. Hope it helps. Android rocks!

Mews answered 5/2, 2014 at 20:49 Comment(8)
any link to a vid with that fluid movement method?Ferrite
@Ferrite I haven't come across any videos of the iMessage scrolling. However, the example gif in the question shows the same effect.Mews
I guess its about developing your own custom ListView. However I already had seen such animation of ListView on some HTC device (being at smartphone settings screens not in some particular app) and noticed it looks pretty cool.Rillis
give a try to JazzyListView which can be found on google play and github github.com/twotoasters/JazzyListView, it has very close to your needs effect called "slideIn".Rillis
@Rillis HTC One? I'll look into it. I just tried implementing JazzyListView. The issue is that it only animates items that are being added to the screen as the scroll is performed. It's also kind of sluggish. I'm going try playing around with it more though.Mews
Hi, I've tried to implement it..I have code which shouldn't be that much far from working..I must leave right now, so feel free to inspire yourself, you should at lest get the idea from that..I've stumbled over some issues, so it is rather messy right now..I'll look into it again when I'll come back home...(It's gridview just because it fits my currently ongoing project) gist.github.com/simekadam/8879528Luteolin
this will never look smooth or quite right without writing your own view.. This is a tough oneDefiant
Did you get solution to this ?Ribbonwood
L
17

This implementation works quite good. There is some flickering though, probably because of altered indices when the adapter add new views to top or bottom..That could be possibly solved by watching for changes in the tree and shifting the indices on the fly..

public class ElasticListView extends GridView implements AbsListView.OnScrollListener,      View.OnTouchListener {

private static int SCROLLING_UP = 1;
private static int SCROLLING_DOWN = 2;

private int mScrollState;
private int mScrollDirection;
private int mTouchedIndex;

private View mTouchedView;

private int mScrollOffset;
private int mStartScrollOffset;

private boolean mAnimate;

private HashMap<View, ViewPropertyAnimator> animatedItems;


public ElasticListView(Context context) {
    super(context);
    init();
}

public ElasticListView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
}

public ElasticListView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init();
}

private void init() {
    mScrollState = SCROLL_STATE_IDLE;
    mScrollDirection = 0;
    mStartScrollOffset = -1;
    mTouchedIndex = Integer.MAX_VALUE;
    mAnimate = true;
    animatedItems = new HashMap<>();
    this.setOnTouchListener(this);
    this.setOnScrollListener(this);

}


@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
    if (mScrollState != scrollState) {
        mScrollState = scrollState;
        mAnimate = true;

    }
    if (scrollState == SCROLL_STATE_IDLE) {
        mStartScrollOffset = Integer.MAX_VALUE;
        mAnimate = true;
        startAnimations();
    }

}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

    if (mScrollState == SCROLL_STATE_TOUCH_SCROLL) {

        if (mStartScrollOffset == Integer.MAX_VALUE) {
            mTouchedView = getChildAt(mTouchedIndex - getPositionForView(getChildAt(0)));
            if (mTouchedView == null) return;

            mStartScrollOffset = mTouchedView.getTop();
        } else if (mTouchedView == null) return;

        mScrollOffset = mTouchedView.getTop() - mStartScrollOffset;
        int tmpScrollDirection;
        if (mScrollOffset > 0) {

            tmpScrollDirection = SCROLLING_UP;

        } else {
            tmpScrollDirection = SCROLLING_DOWN;
        }

        if (mScrollDirection != tmpScrollDirection) {
            startAnimations();
            mScrollDirection = tmpScrollDirection;
        }


        if (Math.abs(mScrollOffset) > 200) {
            mAnimate = false;
            startAnimations();
        }
        Log.d("test", "direction:" + (mScrollDirection == SCROLLING_UP ? "up" : "down") + ", scrollOffset:" + mScrollOffset + ", toucheId:" + mTouchedIndex + ", fvisible:" + firstVisibleItem + ", " +
            "visibleItemCount:" + visibleItemCount + ", " +
            "totalCount:" + totalItemCount);
        int indexOfLastAnimatedItem = mScrollDirection == SCROLLING_DOWN ?
            getPositionForView(getChildAt(0)) + getChildCount() :
            getPositionForView(getChildAt(0));

        //check for bounds
        if (indexOfLastAnimatedItem >= getChildCount()) {
            indexOfLastAnimatedItem = getChildCount() - 1;
        } else if (indexOfLastAnimatedItem < 0) {
            indexOfLastAnimatedItem = 0;
        }

        if (mScrollDirection == SCROLLING_DOWN) {
            setAnimationForScrollingDown(mTouchedIndex - getPositionForView(getChildAt(0)), indexOfLastAnimatedItem, firstVisibleItem);
        } else {
            setAnimationForScrollingUp(mTouchedIndex - getPositionForView(getChildAt(0)), indexOfLastAnimatedItem, firstVisibleItem);
        }
        if (Math.abs(mScrollOffset) > 200) {
            mAnimate = false;
            startAnimations();
            mTouchedView = null;
            mScrollDirection = 0;
            mStartScrollOffset = -1;
            mTouchedIndex = Integer.MAX_VALUE;
            mAnimate = true;
        }
    }
}

private void startAnimations() {
    for (ViewPropertyAnimator animator : animatedItems.values()) {
        animator.start();
    }
    animatedItems.clear();
}

private void setAnimationForScrollingDown(int indexOfTouchedChild, int indexOflastAnimatedChild, int firstVisibleIndex) {
    for (int i = indexOfTouchedChild + 1; i <= indexOflastAnimatedChild; i++) {
        View v = getChildAt(i);
        v.setTranslationY((-1f * mScrollOffset));
        if (!animatedItems.containsKey(v)) {
            animatedItems.put(v, v.animate().translationY(0).setDuration(300).setStartDelay(50 * i));
        }

    }
}

private void setAnimationForScrollingUp(int indexOfTouchedChild, int indexOflastAnimatedChild, int firstVisibleIndex) {
    for (int i = indexOfTouchedChild - 1; i > 0; i--) {
        View v = getChildAt(i);

        v.setTranslationY((-1 * mScrollOffset));
        if (!animatedItems.containsKey(v)) {
            animatedItems.put(v, v.animate().translationY(0).setDuration(300).setStartDelay(50 * (indexOfTouchedChild - i)));
        }

    }
}


@Override
public boolean onTouch(View v, MotionEvent event) {
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            Rect rect = new Rect();
            int childCount = getChildCount();
            int[] listViewCoords = new int[2];
            getLocationOnScreen(listViewCoords);
            int x = (int)event.getRawX() - listViewCoords[0];
            int y = (int)event.getRawY() - listViewCoords[1];
            View child;
            for (int i = 0; i < childCount; i++) {
                child = getChildAt(i);
                child.getHitRect(rect);
                if (rect.contains(x, y)) {
                    mTouchedIndex = getPositionForView(child); 
                    break;
                }
            }
            return false;

    }
    return false;

}

}
Luteolin answered 8/2, 2014 at 16:33 Comment(4)
Sorry about getting back to you so late but I finally got a chance to try this out. Unfortunately it's not working as smoothly as I need. But it's definitely a step forward.Mews
It is nowhere near perfect unfortunately, but it is the way to go IMO..I have been thinking that it could be better to not directly translate items, but instead to scale dividers between them..(add dummy divider view between each two items).. Maybe all those translations at once are causing problems.. I would really like to see someone way more experienced to dig into this. I'm now quite busy, but once I will have some time to spare, I will definitely look again into this. Pushing the limits of Android is what I like to do:)Luteolin
@Luteolin Can you please update your answer, according to your words?Lawrence
is this applicable for scrollview?Bengt
N
16

I've taken just a few minutes to explore this and it looks like it can be done pretty easily with API 12 and above (hopefully I'm not missing something ...). To get the very basic card effect, all it takes is a couple lines of code at the end of getView() in your Adapter right before you return it to the list. Here's the entire Adapter:

    public class MyAdapter extends ArrayAdapter<String>{

        private int mLastPosition;

        public MyAdapter(Context context, ArrayList<String> objects) {
            super(context, 0, objects);
        }

        private class ViewHolder{
            public TextView mTextView;
        }

        @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            ViewHolder holder;

            if (convertView == null) {
                holder = new ViewHolder();
                convertView = LayoutInflater.from(getContext()).inflate(R.layout.grid_item, parent, false);
                holder.mTextView = (TextView) convertView.findViewById(R.id.checkbox);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }

            holder.mTextView.setText(getItem(position));

            // This tells the view where to start based on the direction of the scroll.
            // If the last position to be loaded is <= the current position, we want
            // the views to start below their ending point (500f further down).
            // Otherwise, we start above the ending point.
            float initialTranslation = (mLastPosition <= position ? 500f : -500f);

            convertView.setTranslationY(initialTranslation);
            convertView.animate()
                    .setInterpolator(new DecelerateInterpolator(1.0f))
                    .translationY(0f)
                    .setDuration(300l)
                    .setListener(null);

            // Keep track of the last position we loaded
            mLastPosition = position;

            return convertView;
        }


    }

Note that I'm keeping track of the last position to be loaded (mLastPosition) in order to determine whether to animate the views up from the bottom (if scrolling down) or down from the top (if we're scrolling up).

The wonderful thing is, you can do so much more by just modifying the initial convertView properties (e.g. convertView.setScaleX(float scale)) and the convertView.animate() chain (e.g. .scaleX(float)).

enter image description here

Nice answered 6/2, 2014 at 22:51 Comment(2)
The issue with this is that the animation will only run when getView is called. That means that the views will only animate when they are added as children to the ListView (or when they appear on the screen). I need a way to animate the currently shown ListView items too. However, using the ViewPropertyAnimator of the view does seem to be the ticket in controlling the animation.Mews
force closed for me :(Neurology
I
16

Try this by putting this in your getView() method Just before returning your convertView:

Animation animationY = new TranslateAnimation(0, 0, holder.llParent.getHeight()/4, 0);
animationY.setDuration(1000);
Yourconvertview.startAnimation(animationY);  
animationY = null; 

Where llParent = RootLayout which consists your Custom Row Item.

Interloper answered 11/2, 2014 at 15:27 Comment(0)
K
1

It's honestly going to be a lot of work and quite mathematically intense, but I would have thought you could make the list item's layouts have padding top and bottom and that you could adjust that padding for each item so that the individual items become more or less spaced out. How you would track by how much and how you would know the speed at which the items are being scrolled, well that would be the hard part.

Krissie answered 6/2, 2014 at 21:56 Comment(0)
B
0

Since we do want items to pop every time they appear at the top or bottom of our list, the best place to do it is the getView() method of the adapter:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
    animatePostHc(position, v);
} else {
    animatePreHc(position, v);
}
Bak answered 11/2, 2014 at 14:13 Comment(0)
H
-1

From what I understand what you are looking for is a parallax effect.

This answer is really complete and I think that can help you a lot.

Hundredth answered 14/2, 2014 at 18:5 Comment(0)
H
-1

Use this library: http://nhaarman.github.io/ListViewAnimations

Demo

It is very awesome. Better than the iOS in atleast it is open source :)

Hagood answered 7/1, 2015 at 12:51 Comment(1)
This is for drag drop listview libraryStirrup

© 2022 - 2024 — McMap. All rights reserved.