一個支持item交換動畫的ListView

 

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

 

好了這次的內容就到這裏,我們下次再見。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章