ListView 项目滚动动画(类似“UIKit Dynamics”)

Posted

技术标签:

【中文标题】ListView 项目滚动动画(类似“UIKit Dynamics”)【英文标题】:ListView item scroll animation ("UIKit Dynamics" -like) 【发布时间】:2014-03-02 12:42:47 【问题描述】:

我正在尝试在滚动发生时为 ListView 项目设置动画。更具体地说,我正在尝试在 ios 7 上模拟 iMessage 应用程序的滚动动画。我发现了一个类似的示例 online:

为了澄清,我试图在用户滚动时实现项目的“流畅”移动效果,而不是添加新项目时的动画。我试图修改BaseAdapter 中的视图,并查看了AbsListView 源,看看我是否可以在某处附加一个AccelerateInterpolator 来调整发送到子视图的绘制坐标(如果那样的话甚至是AbsListView 的设计方式)。到目前为止,我一直无法取得任何进展。

有人知道如何复制这种行为吗?


为了帮助使用谷歌搜索记录:这在 ios 上称为 “UIKit Dynamics”

How to replicate Messages bouncing bubbles in iOS 7

它内置于最近的 iOS 版本中。不过使用起来还是有点难度。 (2014) 这是每个人都复制的帖子:widely copied article 令人惊讶的是,UIKit Dynamics 仅适用于苹果的“集合视图”,而不是苹果的“表格视图”,因此所有 iOS debs 都必须将表格视图中的内容转换为“收藏视图”

每个人都使用的库是 BPXLFlowLayout,因为那个人非常擅长复制 iphone 短信应用程序的感觉。事实上,如果你将它移植到 android,我想你可以使用其中的参数来获得相同的感觉。仅供参考,我在我的 android fone 收藏中注意到,HTC 手机在其 UI 上具有这种效果。希望能帮助到你。 Android 摇滚!

【问题讨论】:

任何链接到具有这种流畅移动方法的视频? @pskink 我没有看到任何关于 iMessage 滚动的视频。但是,问题中的示例 gif 显示了相同的效果。 我猜它是关于开发自己的自定义 ListView。但是我已经在某些 HTC 设备上看到过这样的 ListView 动画(在智能手机设置屏幕上,而不是在某些特定应用程序中),并注意到它看起来很酷。 试试JazzyListView,可以在google play和githubgithub.com/twotoasters/JazzyListView上找到,它非常接近你需要的效果,叫做“slideIn”。 @Stan HTC One?我会调查的。我刚刚尝试实现 JazzyListView。问题是它仅在执行滚动时为添加到屏幕的项目设置动画。这也有点迟钝。不过,我会尝试更多地使用它。 【参考方案1】:

这个实现效果很好。虽然有一些闪烁,可能是因为当适配器将新视图添加到顶部或底部时更改了索引。这可以通过观察树中的变化并动态移动索引来解决。..

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;




【讨论】:

很抱歉这么晚才回复你,但我终于有机会试试这个了。不幸的是,它并没有像我需要的那样顺利。但这绝对是一个进步。 不幸的是,它远非完美,但它是 IMO 的必经之路..我一直认为最好不要直接翻译项目,而是在它们之间缩放分隔线..(在每两个项目之间添加虚拟分隔线视图)..也许所有这些翻译一次都会造成问题..我真的很想看到更有经验的人来研究这个。我现在很忙,但是一旦我有空余时间,我一定会再研究一下。突破 Android 的极限是我喜欢做的事情:) @simekadam 你能根据你的话更新你的答案吗? 这适用于滚动视图吗?【参考方案2】:

我只花了几分钟的时间来探索这个,看起来使用 API 12 及更高版本可以很容易地完成(希望我没有遗漏什么......)。要获得非常基本的卡片效果,只需在适配器中的 getView() 末尾写几行代码,然后将其返回列表即可。这是整个适配器:

    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;
        


    

请注意,我正在跟踪要加载的最后一个位置 (mLastPosition),以确定是从底部向上(如果向下滚动)还是从顶部向下(如果我们向上滚动)动画视图)。

奇妙的是,您可以通过修改初始 convertView 属性(例如 convertView.setScaleX(float scale))和 convertView.animate() 链(例如 .scaleX(float))来做更多事情。

【讨论】:

这个问题是动画只会在调用 getView 时运行。这意味着视图只会在它们作为子项添加到 ListView 时(或当它们出现在屏幕上时)才会动画。我也需要一种方法来为当前显示的 ListView 项目设置动画。但是,使用视图的 ViewPropertyAnimator 似乎确实是控制动画的门票。 强制为我关闭:(【参考方案3】:

在返回你的 convertView 之前,把它放在你的 getView() 方法中试试这个:

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

其中 llParent = RootLayout,其中包含您的自定义行项。

【讨论】:

【参考方案4】:

老实说,这将是大量工作并且在数学上非常密集,但我原以为您可以使列表项的布局具有顶部和底部填充,并且您可以调整每个项目的填充,以便各个项目成为或多或少地隔开。您将如何跟踪项目的滚动量以及如何知道滚动的速度,这将是困难的部分。

【讨论】:

【参考方案5】:

由于我们确实希望项目每次出现在列表的顶部或底部时都会弹出,所以最好的地方是适配器的 getView() 方法:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) 
    animatePostHc(position, v);
 else 
    animatePreHc(position, v);

【讨论】:

【参考方案6】:

据我了解,您正在寻找的是视差效果。

This answer 真的很完整,我认为这对你有很大帮助。

【讨论】:

【参考方案7】:

使用这个库:http://nhaarman.github.io/ListViewAnimations

真是太棒了。至少比 iOS 更好,它是开源的 :)

【讨论】:

这是用于拖放列表视图库

以上是关于ListView 项目滚动动画(类似“UIKit Dynamics”)的主要内容,如果未能解决你的问题,请参考以下文章

当代表具有不同的宽度时,QML ListView 滚动不会产生任何动画

停止 ListView 滚动动画

UICollectionVIew:在视图滚动时为单元格设置动画

Flutter ListView 滚动动画重叠兄弟小部件

在 Flutter 中向下滚动 ListView.builder 时出现不需要的动画

当您一直滚动到 ListView.builder 的顶部时,如何禁用动画