ListView item scroll animation (“UIKit Dynamics” -like)

前端 未结 7 878
天命终不由人
天命终不由人 2020-12-22 17:26

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 o

相关标签:
7条回答
  • 2020-12-22 17:53

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

    Demo

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

    0 讨论(0)
  • 2020-12-22 17:56

    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);
    }
    
    0 讨论(0)
  • 2020-12-22 18:03

    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;
    
    }
    
    }
    
    0 讨论(0)
  • 2020-12-22 18:03

    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.

    0 讨论(0)
  • 2020-12-22 18:03

    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.

    0 讨论(0)
  • 2020-12-22 18:04

    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

    0 讨论(0)
提交回复
热议问题