這篇博客帶來一個支持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.如果你覺得這種簡單的交換效果無法滿足你,需要更復雜的交互邏輯,比如通過手指拖拽來交換,沒問題,下一篇博客帶來的控件就實現這種效果,感興趣的可以戳這裏查看。
好了這次的內容就到這裏,我們下次再見。