列表视图项目滚动动画(类似于“UIKit Dynamics”)

64

我试图在滚动时为ListView项添加动画。更具体地说,我正在尝试模仿iOS 7上iMessage应用中的滚动动画。我在网上找到了一个类似的示例:

澄清一下,我试图实现用户滚动时项目上的“流畅”移动效果,而不是添加新项目时的动画。我已经尝试修改我的BaseAdapter中的视图,并查看了AbsListView源代码,看是否可以在某个地方附加AccelerateInterpolator,以调整发送给子视图的绘制坐标(如果这是AbsListView的设计方式的话)。迄今为止,我还没有取得任何进展。

有人有什么想法能够复制这种行为吗?


记录一下以帮助谷歌搜索:这被称为iOS上的"UIKit Dynamics"

如何复制iOS 7中的消息弹跳气泡

它已内置于最近的iOS版本中。然而,它仍然有些难以使用。(2014) 这篇文章是大家都在抄袭的:广受复制的文章. 令人惊讶的是,UIKit Dynamics只能在苹果的"集合视图(collection view)"上使用,而不能在苹果的"表格视图(table view)"上使用,因此所有的iOS开发者都必须将东西从表格视图转换到"集合视图"。

每个人都使用的起点库是BPXLFlowLayout,因为那个人几乎完美地复制了iPhone短信应用程序的感觉。实际上,如果你要将它移植到Android上,我想你可以使用其中的参数来获得相同的感觉。FYI, 我注意到在我的Android手机集合中,HTC手机具有这种效果,在它们的用户界面上。希望它有所帮助。安卓很棒!


有没有任何关于那种流畅运动方法的视频链接? - pskink
@pskink 我还没有看到任何关于iMessage滚动的视频。然而,问题中的示例gif显示了相同的效果。 - b_yng
3
试试JazzyListView,可以在Google Play和GitHub https://github.com/twotoasters/JazzyListView 上找到,它有非常接近您所需的效果,称为“slideIn”。 - Stan
@Stan HTC One?我会研究一下的。我刚试着实现了JazzyListView,但问题是它只能在滚动时添加到屏幕上的项目进行动画处理。而且它有点卡顿。不过我会继续尝试调整它的。 - b_yng
这个问题很棘手,如果不编写自己的视图,它永远不会看起来流畅或完美。 - NightSkyCode
显示剩余3条评论
7个回答

17

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

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;

}

}

非常抱歉回复晚了,但我终于有机会尝试了一下。不幸的是,它并没有像我需要的那样顺利运行。但这无疑是一个进步。 - b_yng
1
很不幸,它还远非完美,但在我看来这是可行的方式。我一直在思考,不直接翻译项目,而是在它们之间添加分隔符(在每两个项目之间添加虚拟分隔符视图)可能会更好。也许所有这些翻译一次性进行会导致问题。我真的希望能看到更有经验的人深入研究这个问题。我现在很忙,但一旦我有一些空闲时间,我一定会再次研究这个问题。挑战Android的极限是我喜欢做的事情 :) - simekadam
@simekadam,您能否根据您的话更新您的答案? - shadyinside
这适用于ScrollView吗? - Star

16

我只花了几分钟的时间去探索这个东西,看起来可以在API 12及以上版本中轻松完成(希望我没有漏掉什么……)。为了获得非常基本的卡片效果,在您的Adapter的getView()方法返回列表之前,只需要在其结尾添加几行代码即可。以下是整个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;
        }


    }

注意,我正在追踪最后加载的位置(mLastPosition),以确定是否向上从底部(如果向下滚动)或向下从顶部(如果我们向上滚动)动画显示视图。

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

输入图像描述


3
问题在于:动画仅在调用getView时运行。这意味着当视图被添加为ListView的子项(或出现在屏幕上)时,它们才会进行动画。我需要一种方法来对当前显示的ListView项目进行动画。但是,使用View的ViewPropertyAnimator似乎是控制动画的有效方法。 - b_yng
我的应用程序崩溃了 :( - Hamid

16

在返回convertView之前,在您的getView()方法中添加以下内容,以尝试此操作:

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

当llParent = RootLayout时,它包含您的自定义行项目。


1
“这个任务需要大量的工作和数学计算,但我认为你可以给列表项设置上下内边距,并调整每个项目的内边距,使得各个项目之间的间距更加均匀。如何追踪每个项目之间的间距以及如何知道项目滚动的速度是困难的部分。”

0

由于我们希望每次在列表顶部或底部出现时都弹出项目,因此最好的地方是在适配器的getView()方法中执行:

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

-1

这是用于拖放列表视图库的内容。 - Tarit Ray

-1
据我所知,您正在寻找的是视差效果。 这个答案非常完整,我认为可以帮助您很多。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接