ListView item scroll animation ("UIKit Dynamics" -like)

AndroidAndroid ListviewAndroid Animation

Android Problem Overview


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.

https://stackoverflow.com/questions/19795191/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!

Android Solutions


Solution 1 - Android

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;

}

}

Solution 2 - Android

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

Solution 3 - Android

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.

Solution 4 - Android

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.

Solution 5 - Android

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);
}

Solution 6 - Android

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.

Solution 7 - Android

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

Demo

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionb_yngView Question on Stackoverflow
Solution 1 - AndroidsimekadamView Answer on Stackoverflow
Solution 2 - AndroidJustin PollardView Answer on Stackoverflow
Solution 3 - AndroidSagar ShahView Answer on Stackoverflow
Solution 4 - AndroidSDJMcHattieView Answer on Stackoverflow
Solution 5 - AndroidgargAmanView Answer on Stackoverflow
Solution 6 - AndroidDynaView Answer on Stackoverflow
Solution 7 - AndroidVedant AgarwalaView Answer on Stackoverflow