我正在尝试在滚动发生时为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摇滚!
答案 0 :(得分: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;
}
}
答案 1 :(得分:16)
我花了几分钟时间来探索这个问题,看起来它可以很容易地用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)),就可以做得更多。
答案 2 :(得分:16)
尝试将此文件放入getView()方法中,然后再返回convertView:
Animation animationY = new TranslateAnimation(0, 0, holder.llParent.getHeight()/4, 0);
animationY.setDuration(1000);
Yourconvertview.startAnimation(animationY);
animationY = null;
llParent = RootLayout,其中包含自定义行项目。
答案 3 :(得分:1)
老实说,这将是一项很多工作,并且在数学上非常强烈,但我认为你可以让列表项的布局有填充顶部和底部,你可以调整每个项目的填充,以便单个项目成为或多或少间隔开。你将如何跟踪多少以及如何知道项目滚动的速度,这将是困难的部分。
答案 4 :(得分:0)
由于我们确实希望每次出现在列表的顶部或底部时弹出项目,因此最好的位置是适配器的getView()方法:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
animatePostHc(position, v);
} else {
animatePreHc(position, v);
}
答案 5 :(得分:-1)
根据我的理解,你所寻找的是一种视差效应。
This answer非常完整,我认为这可以帮到你很多。
答案 6 :(得分:-1)