这篇博客带来一个支持item交换动画的ListView。话不多说,先上效果。
实现的效果就是,通过手动操作交换ListView中两个item的位置,并在交换时附带移动交换效果。之前在知乎的app上看到过这种效果,顺手写了一个,搬运上来跟大家分享一下。
核心组件就一个 TranslationListView.java :
public class TranslationListView extends ListView { /** * 速度模板,影响视图移动时的速度变化 * * MODE_LINEAR // 线性变化模式 * MODE_ACCELERATE // 加速模式 * MODE_DECELERATE // 减速模式 * MODE_ACCELERATE_DECELERATE // 先加速后加速模式 * */ public static final int MODE_LINEAR = 0x001; public static final int MODE_ACCELERATE = 0x002; public static final int MODE_DECELERATE = 0x003; public static final int MODE_ACCELERATE_DECELERATE = 0x004; /** * 可设置选项 */ // 移动动画储持续时间,单位毫秒 private long duration = 300; // 速度模板 private int speedMode = MODE_ACCELERATE_DECELERATE; // 移动监听接口 private OnTranslateListener translateListener; // 移动锁定标识 private boolean isLock; public TranslationListView(Context context) { super(context); } public TranslationListView(Context context, AttributeSet attrs) { super(context, attrs); } public TranslationListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 将指定项上移一格 * * @param position 指定项的序坐标 */ public void moveUp(int position) { translate(position, position - 1); } /** * 将指定项下移一格 * * @param position 指定项的坐标 */ public void moveDown(int position) { translate(position, position + 1); } /** * 执行移动方法 * * @param fromPosition // 起始坐标 * @param toPosition // 终点坐标 */ private void translate(final int fromPosition, final int toPosition) { if (isLock) { return; } View fromView = getChildAt(fromPosition - getFirstVisiblePosition()); View toView = getChildAt(toPosition - getFirstVisiblePosition()); if (fromView == null || toView == null) { return; } float distance = toView.getY() - fromView.getY(); ObjectAnimator fromAnimator = ObjectAnimator.ofFloat(fromView, "translationY", 0, distance); ObjectAnimator toAnimator = ObjectAnimator.ofFloat(toView, "translationY", 0, -distance); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether(Arrays.asList(new Animator[]{fromAnimator, toAnimator})); animatorSet.setDuration(duration); animatorSet.setInterpolator(getAnimatorInterpolator()); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { isLock = true; } @Override public void onAnimationEnd(Animator animation) { isLock = false; // 动画播放结束时将指定项的translationY属性重置 // 此时调用者需手动把两项的数据调换,并刷新列表,实现真的把两项位置调换的目的 resetTranslate(fromPosition,toPosition); if (translateListener != null) { translateListener.onFinish(fromPosition, toPosition); } } }); animatorSet.start(); } /** * 重置移动项的translationY属性 * * @param fromPosition 起始坐标 * @param toPosition 终点坐标 */ private void resetTranslate(int fromPosition, int toPosition) { View fromView = getChildAt(fromPosition - getFirstVisiblePosition()); View toView = getChildAt(toPosition - getFirstVisiblePosition()); if (fromView != null && toView != null) { fromView.setTranslationY(0); toView.setTranslationY(0); } } /** * 根据速度模板创建动画迭代器 * * @return */ private Interpolator getAnimatorInterpolator() { switch (speedMode) { case MODE_LINEAR: return new LinearInterpolator(); case MODE_ACCELERATE: return new AccelerateInterpolator(); case MODE_DECELERATE: return new DecelerateInterpolator(); case MODE_ACCELERATE_DECELERATE: return new AccelerateDecelerateInterpolator(); default: return null; } } /** * 设置移动动画持续时间 * * @param duration 时间,单位毫秒 */ public void setDuration(long duration) { this.duration = duration; } /** * 设置速度模式,可选项: * MODE_LINEAR 线性变化模式 * MODE_ACCELERATE 加速模式 * MODE_DECELERATE 减速模式 * MODE_ACCELERATE_DECELERATE 先加速后加速模式 * * @param speedMode */ public void setSpeedMode(int speedMode) { this.speedMode = speedMode; } /** * 设置移动监听接口 * * @param translateListener */ public void setTranslateListener(OnTranslateListener translateListener) { this.translateListener = translateListener; } /** * 移动监听接口 */ public interface OnTranslateListener { /** * 移动动画结束时回调 * * @param fromPosition 移动项起始坐标 * @param toPosition 移动项终点坐标 */ void onFinish(int fromPosition, int toPosition); } }实现原理很简单,核心代码就这两句:
ObjectAnimator fromAnimator = ObjectAnimator.ofFloat(fromView, "translationY", 0, distance); ObjectAnimator toAnimator = ObjectAnimator.ofFloat(toView, "translationY", 0, -distance);即创建一个属性动画不断地变更需要移动的item的translationY属性,来实现让其在屏幕上移动的目的。
然后看一下使用。既然是ListView,使用的时候自然需要一个Adapter和对应的item布局。
先定义一个item布局 “item_test_listview.xml” :
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#fffafa" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:id="@+id/content_textview" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="15dp" android:layout_weight="1" android:maxLines="1" android:text="THIS IS CONTENT" android:textSize="16sp" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="15dp" android:orientation="vertical"> <TextView android:id="@+id/move_up_textview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginBottom="15dp" android:text="MOVE UP" android:textColor="@color/colorAccent" android:textSize="12dp" /> <TextView android:id="@+id/move_down_textview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:text="MOVE DOWN" android:textColor="@color/colorAccent" android:textSize="12dp" /> </LinearLayout> </LinearLayout>再写一个适配器 “TestAdapter.java”:
public class TestAdapter extends BaseAdapter { private Context context; private int resourceId; private ArrayList<String> list; private OnItemClickListener listener; public TestAdapter(Context context, int resourceId, ArrayList<String> list) { this.context = context; this.resourceId = resourceId; this.list = list; } @Override public int getCount() { return list.size(); } @Override public Object getItem(int position) { return list.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(final int position, View convertView, ViewGroup parent) { View view; ViewHolder viewHolder; if (convertView == null) { view = LayoutInflater.from(context).inflate(resourceId, null); viewHolder = new ViewHolder(); viewHolder.contentTextView = view.findViewById(R.id.content_textview); viewHolder.moveUpTextView = view.findViewById(R.id.move_up_textview); viewHolder.moveDownTextView = view.findViewById(R.id.move_down_textview); view.setTag(viewHolder); } else { view = convertView; viewHolder = (ViewHolder) view.getTag(); } viewHolder.contentTextView.setText(list.get(position)); viewHolder.moveUpTextView.setVisibility(position == 0 ? View.INVISIBLE : View.VISIBLE); viewHolder.moveDownTextView.setVisibility(position == list.size() - 1 ? View.INVISIBLE : View.VISIBLE); viewHolder.moveUpTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (listener != null) { listener.onMoveUpClick(position); } } }); viewHolder.moveDownTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (listener != null) { listener.onMoveDownClick(position); } } }); return view; } public interface OnItemClickListener { void onMoveUpClick(int position); void onMoveDownClick(int position); } public void setOnItemClickListener(OnItemClickListener listener) { this.listener = listener; } class ViewHolder { TextView contentTextView; TextView moveUpTextView; TextView moveDownTextView; } }代码很简单,没什么好讲的。
然后就可以用了,在MainActivity里这样写:
public class MainActivity extends AppCompatActivity { private TranslationListView translationListView; private TestAdapter adapter; private ArrayList<String> list; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initData(); initView(); } private void initData() { list = new ArrayList<>(); for (int i = 1; i <= 20; i++) { list.add("我是第" + i + "条内容嘿嘿嘿"); } } private void initView() { translationListView = findViewById(R.id.translate_listview); adapter = new TestAdapter(this, R.layout.item_test_listview, list); adapter.setOnItemClickListener(new TestAdapter.OnItemClickListener() { @Override public void onMoveUpClick(int position) { translationListView.moveUp(position); } @Override public void onMoveDownClick(int position) { translationListView.moveDown(position); } }); translationListView.setAdapter(adapter); translationListView.setTranslateListener(new TranslationListView.OnTranslateListener() { @Override public void onFinish(int fromPosition, int toPosition) { exchangeData(fromPosition, toPosition, list); adapter.notifyDataSetChanged(); } }); } private void exchangeData(int position1, int position2, List list) { Object object = list.get(position2); list.set(position2, list.get(position1)); list.set(position1, object); } }TranslationListView提供两个方法moveUp和moveDown,分别实现让指定item上移和下移。
需要特别注意的是,控件只实现了让item移动的动画,并没有真的把item数据的顺序调换过来。所以使用的时候需要监听移动接口OnTranslateListener,在移动动画完成的时候手动调换数据并刷新列表,来实现真的把两个item调换过来的目的。也就是这几句代码:
translationListView.setTranslateListener(new TranslationListView.OnTranslateListener() { @Override public void onFinish(int fromPosition, int toPosition) { exchangeData(fromPosition, toPosition, list); adapter.notifyDataSetChanged(); } });
然后运行一下就能看到开头的效果了。
最后附上源码地址:https://download.csdn.net/download/Sure_Min/12566430
Ps.如果你觉得这种简单的交换效果无法满足你,需要更复杂的交互逻辑,比如通过手指拖拽来交换,没问题,下一篇博客带来的控件就实现这种效果,感兴趣的可以戳这里查看。
好了这次的内容就到这里,我们下次再见。