Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義控件——可拖拽排序的ListView

自定義控件——可拖拽排序的ListView

編輯:關於Android編程

前言

最經研究了一下拖拽排序的ListView,跟酷狗裡的播放列表排序一樣,但因為要添加自己特有的功能,所以研究了好長時間。一開始接觸的是GitHub的開源項目——DragSortListView,實現的效果和流暢度都很棒。想根據他的代碼自己寫一個,但代碼太多了,實現的好復雜,看別人的代碼你懂的了,就去嘗試尋找其他辦法。最後還是找到了更簡單的實現方法,雖然跟開源項目比要差一點,但對我來說可以了,最重要的是完全可以自定義。

實現的效果如下:
這裡寫圖片描述

主要問題

如何根據觸摸的位置確定是哪個條目?
ListView有一個方法,可以根據ListView控件內的坐標位置確定條目索引:

int position = pointToPosition(int x, int y)

如何把此條目View的提取出來(我稱之為快照)?
ListView可通過getChildAt(int index)來獲取子控件。但因為ListView內的條目View都要復用,所以此index不等於pointToPosition(x, y)獲取的位置,要減去第一個可見條目的位置。即:

View itemView = getChildAt(position - getFirstVisiblePosition());

獲取到View後,要把它變成一張照片(快照),View中有自帶的方法,可以把View的當前顯示的界面保存為Bitmap圖片:

// 進行繪圖緩存
itemView.setDrawingCacheEnabled(true);
// 提取緩存中的圖片
Bitmap bitmap = Bitmap.createBitmap(itemView.getDrawingCache());

如何懸浮在窗口上,並跟著手移動?
有了View的圖片,可通過ImageView顯示出來,但如何懸浮在窗口上?這裡需要使用WindowManager來顯示,並設置其參數WindowManager.LayoutParams。跟平常用代碼在ViewGroup中添加View一樣。

ImageView mDragPhotoView= new ImageView(getContext());
mDragPhotoView.setImageBitmap(mDragPhotoBitmap);
// 獲取當前窗口管理器
WindowManager mWindowManager= (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams mWindowLayoutParams= new WindowManager.LayoutParams();
wm.addView(mDragPhotoView, mWindowLayoutParams);

至於跟著手移動,手觸摸的坐標知道了,通過mWindowLayoutParams.y來設置y的坐標,並更新到界面上:

mWindowLayoutParams.y = newY;
mWindowManager.updateViewLayout(mDragPhotoView, mWindowLayoutParams);

條目超過ListView,如何滾動?
ListView有很多方法可以實現滾動:
smoothScrollBy(int distance, int duration)實現緩慢移動;
setSelectionFromTop(int position, int y)來設定指定條目距離頂部位置。
為了美觀,我這裡選擇了第一種方法

以上4點就是最重要的,下面的主要是為了增加功能和提升用戶體驗

如何讓移動到的位置,不顯示條目,並與之前的位置進行交換?
不顯示條目,也就是不顯示View,但位置還得存在,這裡可以使用View的setVisibility()來實現:
setVisibility(View.INVISIBLE)
交換位置就是適配器中的數據進行交換,我這裡自定義了一個BaseAdapter子抽象類,並在內部實現了調換位置的方法。當然也可以使用List的先刪除remove(int position),後添加add(int location, Object object)的方法。

public void swapData(int from, int to){
    // mDragDatas是List實例對象
    Collections.swap(mDragDatas, from, to);
    notifyDataSetChanged();
}

如何讓快照只能在ListView中的可視條目范圍內移動?
從此開始的問題,參考的資料中幾乎沒有,自己另外添加的功能,覺得能提升用戶體驗。

快照必須跟條目一樣,在ListView控件范圍內,但快照的坐標是針對屏幕的。在onTouchEvent()裡ev.getY()獲取的是觸摸點在控件內的Y軸坐標,ev.getRawY()獲取的是在屏幕內的Y軸坐標點,so

mRawOffsetY = (int) (ev.getRawY() - mDownY);

就是ListView的左上角Y坐標,也就是快照的Y軸的最小值。

ListView的getHeight()就能獲取底部高度,條目的總高度itemHeight是知道的(代碼中,分割線的高度忘了加了,如果很小的話,不會有什麼影響)。Y軸的最大值就是:

mRawOffsetY + getHeight() - mDragItemHeight;

但有一點,如果條目很少,都沒填充完ListView,怎麼辦?我們可以使用條目總高度*條目數量來確定所有條目的高度,與ListView的高度進行對比。這裡,我用條目高度+分割線高度的辦法來確定條目總高度。當然也可以使用一個條目的top到下一個條目的top距離來確定每個條目占的總高度。
這裡寫圖片描述

/**
 * 判斷ListView是否全部顯示,即ListView無法上下滾動了
 */
private boolean isShowAll() {
       if (getChildCount() == 0) {
           return true;
       }
       View firstChild = getChildAt(0);
       int itemAllHeight = firstChild.getBottom() - firstChild.getTop() + getDividerHeight();
       return itemAllHeight * getAdapter().getCount() < getHeight();
}

...

// 根據是否顯示完全,設定快照在Y軸上可拖到的最大值
if (isShowAll()) {
    mMaxDragY = mRawOffsetY + getChildAt(getAdapter().getCount() - 1).getTop();
} else {
    mMaxDragY = mRawOffsetY + getHeight() - mDragItemHeight;
}

如果條目很多,在拖拽時,有時需要快速滾動,有時需要慢速滾動,如何實現?
原理就是根據快照的位置距離上下邊緣的位置,如果距離小於一個條目的高度,開始滾動,越靠近邊緣滾動的越快。可通過設置smoothScrollBy(distance, duration)中的distance來達到調速的效果。設定一個在邊緣時滾動的最大值,剩下的就是按比例來計算了。百分比計算參考下面的”主要代碼”(不會用標簽跳過去,知道的大俠麻煩告訴一聲,謝謝)

// 如果當前位置已經不到一個條目,則進行上或下的滾動。並根據距離邊界的距離,設定滾動速度
int dragY = mMoveY - mItemOffsetY;
if (dragY < mDragItemHeight) {
    int value = Math.max(0, dragY); // 防越界
    float percent = estimatePercent(mDragItemHeight, 0, value);
    int distance = estimateInt(0, -mMaxDistance, percent);
    smoothScrollBy(distance, SMOOTH_SCROLL_DURATION);
} else if (dragY > getHeight() - 2 * mDragItemHeight) {
    int value = Math.max(0, getHeight() - dragY - mDragItemHeight); // 防越界
    float percent = estimatePercent(mDragItemHeight, 0, value);
    int distance = estimateInt(0, mMaxDistance, percent);
    smoothScrollBy(distance, SMOOTH_SCROLL_DURATION);

使用setVisibility(),把當前坐標的條目隱藏時,會出現閃爍,如何解決?
在觸摸下去的時候,被觸摸的條目設置了隱藏,快照顯示出來前會有一段空白,導致閃爍的情況。個人覺得可能是快照還沒完全顯示出來。試了很多方法都不如意,最後決定還是用動畫的來去閃爍。

// 隱藏。為了防止隱藏時出現畫面閃爍,使用動畫去除閃爍效果
Animation aAnim = new AlphaAnimation(1f, DRAG_PHOTO_VIEW_ALPHA);
aAnim.setDuration(50);
aAnim.setAnimationListener(new Animation.AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) {
    }

    @Override
    public void onAnimationEnd(Animation animation) {
    // Move中有隱藏的功能,如果按下後快速移動,會出現該顯示的又被隱藏了。所以要作判斷
        if (mIsDraging && mToPosition == mDragPosition) {
            itemView.setVisibility(View.INVISIBLE);
        }
    }

    @Override
    public void onAnimationRepeat(Animation animation) {
    }
});
itemView.startAnimation(aAnim);

主要代碼

開源項目中發現老外的代碼注釋很多,覺得還是很有必要的。上次自己寫了一個自定義控件,涉及到一些數學公式,幾個星期後要改進,結果自己都無法看懂了,最後使用了另外的方法去解決。

DragListView.java:

package com.zjun.draglistview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.support.annotation.FloatRange;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;


/**
 * 可拖拽排序ListView
 * Created by Ralap on 2016/5/8.
 */
public class DragListView extends ListView {
    private static final String LOG_TAG = "DragListView";

    /**
     * 拖拽快照的透明度(0.0f ~ 1.0f)。
     */
    private static final float DRAG_PHOTO_VIEW_ALPHA = .8f;

    /**
     * 上下滾動時的時間
     */
    private static final int SMOOTH_SCROLL_DURATION = 100;

    /**
     * 上下滾動時的最大距離,可進行設置
     * @see #setMaxDistance(int)
     * @see #getMaxDistance()
     */
    private int mMaxDistance = 30;

    /**
     * 是否處於拖拽中
     */
    private boolean mIsDraging;

    /**
     * 按下時的坐標位置
     */
    private int mDownX;
    private int mDownY;

    /**
     * 移動時的坐標
     */
    private int mMoveX;
    private int mMoveY;

    /**
     * 原生偏移量。也就是ListView的左上角相對於屏幕的位置
     */
    private int mRawOffsetX;
    private int mRawOffsetY;

    /**
     * 在條目中的位置
     */
    private int mItemOffsetX;
    private int mItemOffsetY;

    /**
     * 拖拽快照的垂直位置范圍。根據條目數量和ListView的高度來確定
     */
    private int mMinDragY;
    private int mMaxDragY;

    /**
     * 拖拽條目的高度
     */
    private int mDragItemHeight;

    /**
     * 被拖拽的條目位置
     */
    private int mDragPosition;

    /**
     * 移動前的條目位置
     */
    private int mFromPosition;

    /**
     * 移動後的條目位置
     */
    private int mToPosition;

    /**
     * 窗口管理器,用於顯示條目的快照
     */
    private WindowManager mWindowManager;

    /**
     * 窗口管理的布局參數
     */
    private WindowManager.LayoutParams mWindowLayoutParams;

    /**
     * 拖拽條目的快照圖片
     */
    private Bitmap mDragPhotoBitmap;

    /**
     * 正在拖拽的條目快照view
     */
    private ImageView mDragPhotoView;


    public DragListView(Context context) {
        super(context);
    }

    public DragListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public DragListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // 獲取第一個手指點的Action
        int action = ev.getAction() & MotionEvent.ACTION_MASK;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mDownX = (int) ev.getX();
                mDownY = (int) ev.getY();
                // 獲取當前觸摸位置對應的條目索引
                mDragPosition = pointToPosition(mDownX, mDownY);
                // 如果觸摸的坐標不在條目上,在分割線、或外部區域,則為無效值-1; 寬度3/4 以右的區域可拖拽
                if (mDragPosition == AdapterView.INVALID_POSITION || mDownX < getWidth() * 3 / 4) {
                    return super.onTouchEvent(ev);
                }
                mIsDraging = true;
                mToPosition = mFromPosition = mDragPosition;

                mRawOffsetX = (int) (ev.getRawX() - mDownX);
                mRawOffsetY = (int) (ev.getRawY() - mDownY);

                // 開始拖拽的前期工作:展示item快照
                startDrag();
                break;

            case MotionEvent.ACTION_MOVE:
                mMoveX = (int) ev.getX();
                mMoveY = (int) ev.getY();
                if (mIsDraging) {
                    // 更新快照位置
                    updateDragView();
                    // 更新當前被替換的位置
                    updateItemView();
                } else {
                    return super.onTouchEvent(ev);
                }
                break;

            case MotionEvent.ACTION_UP:
                if (mIsDraging) {
                    // 停止拖拽
                    stopDrag();
                } else {
                    return super.onTouchEvent(ev);
                }
                break;
            default:
                break;
        }
        return true;
    }

    /**
     * 開始拖拽
     */
    private boolean startDrag() {
        // 實際在ListView中的位置,因為涉及到條目的復用
        final View itemView = getItemView(mDragPosition);
        if (itemView == null) {
            return false;
        }
        // 進行繪圖緩存
        itemView.setDrawingCacheEnabled(true);
        // 提取緩存中的圖片
        mDragPhotoBitmap = Bitmap.createBitmap(itemView.getDrawingCache());
        // 清除繪圖緩存,否則復用的時候,會出現前一次的圖片。或使用銷毀destroyDrawingCache()
        itemView.setDrawingCacheEnabled(false);

        // 隱藏。為了防止隱藏時出現畫面閃爍,使用動畫去除閃爍效果
        Animation aAnim = new AlphaAnimation(1f, DRAG_PHOTO_VIEW_ALPHA);
        aAnim.setDuration(50);
        aAnim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                // Move中有隱藏的功能,如果按下後快速移動,會出現該顯示的又被隱藏了。所以要作判斷
                if (mIsDraging && mToPosition == mDragPosition) {
                    itemView.setVisibility(View.INVISIBLE);
                }
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }
        });
        itemView.startAnimation(aAnim);

        mItemOffsetX = mDownX - itemView.getLeft();
        mItemOffsetY = mDownY - itemView.getTop();
        mDragItemHeight = itemView.getHeight();
        mMinDragY = mRawOffsetY;
        // 根據是否顯示完全,設定快照在Y軸上可拖到的最大值
        if (isShowAll()) {
            mMaxDragY = mRawOffsetY + getChildAt(getAdapter().getCount() - 1).getTop();
        } else {
            mMaxDragY = mRawOffsetY + getHeight() - mDragItemHeight;
        }
        createDragPhotoView();
        return true;
    }

    /**
     * 判斷ListView是否全部顯示,即ListView無法上下滾動了
     */
    private boolean isShowAll() {
        if (getChildCount() == 0) {
            return true;
        }
        View firstChild = getChildAt(0);
        int itemAllHeight = firstChild.getBottom() - firstChild.getTop() + getDividerHeight();
        return itemAllHeight * getAdapter().getCount() < getHeight();
    }

    /**
     * 創建拖拽快照
     */
    private void createDragPhotoView() {
        // 獲取當前窗口管理器
        mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        // 創建布局參數
        mWindowLayoutParams = new WindowManager.LayoutParams();
        mWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        mWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        mWindowLayoutParams.gravity = Gravity.TOP | Gravity.START;
        mWindowLayoutParams.format = PixelFormat.TRANSLUCENT; // 期望的圖片為半透明效果,但設置其他值並沒有看到不一樣的效果
        // 下面這些參數能夠幫助准確定位到選中項點擊位置
        mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
        mWindowLayoutParams.windowAnimations = 0; // 無動畫
        mWindowLayoutParams.alpha = DRAG_PHOTO_VIEW_ALPHA; // 微透明

        mWindowLayoutParams.x = mDownX + mRawOffsetX - mItemOffsetX;
        mWindowLayoutParams.y = adjustDragY(mDownY + mRawOffsetY - mItemOffsetY);

        mDragPhotoView = new ImageView(getContext());
        mDragPhotoView.setImageBitmap(mDragPhotoBitmap);
        mWindowManager.addView(mDragPhotoView, mWindowLayoutParams);
    }

    /**
     * 校正Drag的值,不讓其越界
     */
    private int adjustDragY(int y) {
        if (y < mMinDragY) {
            return mMinDragY;
        } else if (y > mMaxDragY) {
            return mMaxDragY;
        }
        return y;
    }

    /**
     * 根據Adapter中的位置獲取對應ListView的條目
     */
    private View getItemView(int position) {
        if (position < 0 || position >= getAdapter().getCount()) {
            return null;
        }
        int index = position - getFirstVisiblePosition();
        return getChildAt(index);
    }

    /**
     * 更新快照的位置
     */
    private void updateDragView() {
        if (mDragPhotoView != null) {
            mWindowLayoutParams.y = adjustDragY(mMoveY + mRawOffsetY - mItemOffsetY);
            mWindowManager.updateViewLayout(mDragPhotoView, mWindowLayoutParams);
        }
    }

    /**
     * 更新條目位置、顯示等
     */
    private void updateItemView() {
        int position = pointToPosition(mMoveX, mMoveY);
        if (position != AdapterView.INVALID_POSITION) {
            mToPosition = position;
        }

        // 調換位置,並把顯示進行調換
        if (mFromPosition != mToPosition) {
            if (exchangePosition()) {
                View view = getItemView(mFromPosition);
                if (view != null) {
                    view.setVisibility(View.VISIBLE);
                }
                view = getItemView(mToPosition);
                if (view != null) {
                    view.setVisibility(View.INVISIBLE);
                }
                mFromPosition = mToPosition;
            }
        }

        // 如果當前位置已經不到一個條目,則進行上或下的滾動。並根據距離邊界的距離,設定滾動速度
        int dragY = mMoveY - mItemOffsetY;
        if (dragY < mDragItemHeight) {
            int value = Math.max(0, dragY); // 防越界
            float percent = estimatePercent(mDragItemHeight, 0, value);
            int distance = estimateInt(0, -mMaxDistance, percent);
            smoothScrollBy(distance, SMOOTH_SCROLL_DURATION);
        } else if (dragY > getHeight() - 2 * mDragItemHeight) {
            int value = Math.max(0, getHeight() - dragY - mDragItemHeight); // 防越界
            float percent = estimatePercent(mDragItemHeight, 0, value);
            int distance = estimateInt(0, mMaxDistance, percent);
            smoothScrollBy(distance, SMOOTH_SCROLL_DURATION);
        }
    }

    /**
     * 停止拖拽
     */
    private void stopDrag() {
        // 顯示坐標上的條目
        View view = getItemView(mToPosition);
        if (view != null) {
            view.setVisibility(View.VISIBLE);
        }
        // 移除快照
        if (mDragPhotoView != null) {
            mWindowManager.removeView(mDragPhotoView);
            mDragPhotoView.setImageDrawable(null);
            mDragPhotoBitmap.recycle();
            mDragPhotoBitmap = null;
            mDragPhotoView = null;
        }
        mIsDraging = false;
    }

    /**
     * 調換位置
     */
    private boolean exchangePosition() {
        int itemCount = getAdapter().getCount();
        if (mFromPosition >= 0 && mFromPosition < itemCount
                && mToPosition >= 0 && mToPosition < itemCount) {
             getAdapter().swapData(mFromPosition, mToPosition);
            return true;
        }
        return false;
    }


    /**
     * 根據百分比,估算在指定范圍內的值
     */
    public static int estimateInt(int start ,int end, @FloatRange(from = 0.0f, to = 1.0f) float percent) {
        return (int) (start + percent * (end - start));
    }

    /**
     * 估算給定值在指定范圍內的百分比
     * @param start 始值
     * @param end 末值
     * @param value 要估算的值
     * @return 0.0f ~ 1.0f。如果沒有指定范圍,或給定值不在范圍內則返回-1
     */
    public static float estimatePercent(float start, float end, float value) {
        if (start == end
                || (value < start && value < end)
                || (value > start && value > end)){
            return -1;
        }
        return (value - start) / (end - start);
    }

    @Override
    public void setAdapter(ListAdapter adapter) {
        if (!(adapter instanceof DragListViewAdapter)){
            throw new RuntimeException("請使用自帶的Adapter");
        }
        super.setAdapter(adapter);
    }

    @Override
    public DragListViewAdapter getAdapter(){
        return (DragListViewAdapter) super.getAdapter();
    }
}

MainActivity.java

private void initView() {
    dvl_drag_list = (DragListView) findViewById(R.id.dvl_drag_list);
    tv_msg_drag_list = (TextView) findViewById(R.id.tv_msg_drag_list);
    tv_msg_drag_list.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            int size = mDataList.size();
            String dataMsg;
            if (size == 0) {
                dataMsg = "沒有數據了";
            } else {
                dataMsg = "數據大小:" + mDataList.size() + ", 最後一個:" + mDataList.get(mDataList.size() - 1);
            }
            tv_msg_drag_list.setText(dataMsg);
        }
    });

    mListAdapter = new MyAdapter(this, mDataList);
    dvl_drag_list.setAdapter(mListAdapter);
}

public class MyAdapter extends DragListViewAdapter {

    public MyAdapter(Context context, List dataList) {
        super(context, dataList);
    }

    @Override
    public View getItemView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;
        if (convertView == null) {
            convertView = LayoutInflater.from(getApplicationContext()).inflate(R.layout.item_drag_list, parent, false);
            viewHolder = new ViewHolder();
            viewHolder.name = (TextView) convertView.findViewById(R.id.tv_name_drag_list);
            viewHolder.desc = (TextView) convertView.findViewById(R.id.tv_desc_drag_list);
            convertView.setTag(viewHolder);
        }else{
            viewHolder = (ViewHolder) convertView.getTag();
        }
        viewHolder.name.setText(mDragDatas.get(position));
        String s = mDragDatas.get(position) + "的描述";
        viewHolder.desc.setText(s);
        return convertView;
    }

    class ViewHolder{
        TextView name;
        TextView desc;
    }

}

DragListViewAdapter.java

public abstract class DragListViewAdapter extends BaseAdapter{

    public Context mContext;
    public List mDragDatas;

    public DragListViewAdapter(Context context, List dataList){
        this.mContext = context;
        this.mDragDatas = dataList;
    }

    @Override
    public int getCount() {
        return mDragDatas == null ? 0 : mDragDatas.size();
    }

    @Override
    public T getItem(int position) {
        return mDragDatas.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        return getItemView(position, convertView, parent);
    }

    public abstract View getItemView(int position, View convertView, ViewGroup parent);

    public void swapData(int from, int to){
        Collections.swap(mDragDatas, from, to);
        notifyDataSetChanged();
    }

    public void deleteData(int position) {
        mDragDatas.remove(position);
        notifyDataSetChanged();
    }
}

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved