帶你一步步實現可拖拽的GridView控件

博文出處:帶你一步步實現可拖拽的GridView控件,歡迎大家關注我的博客,謝謝!

經常使用網易新聞的童鞋都知道在網易新聞中有一個新聞欄目管理,其中GridView的item是可以拖拽的,效果十分炫酷。具體效果如下圖:

新聞欄目管理效果圖gif

是不是也想自己也想實現出相同的效果呢?那就一起來往下看吧。

首先我們來梳理一下思路:

  1. 當用戶長按選擇一個item時,將該item隱藏,然后用WindowManager添加一個新的window,該window與所選擇item一模一樣,并且跟隨用戶手指滑動而不斷改變位置。
  2. 當window的位置坐標在GridView里面時,使用pointToPosition (int x, int y)方法來判斷對應的應該是哪個item,在adapter中作出數據集相應的變化,然后做出平移的動畫。
  3. 當用戶手指抬起時,把window移除,使用notifyDataSetChanged()做出GridView更新。

講完了思路后,我們就來實踐一下吧,把這個控件取名為DragGridView。

public DragGridView(Context context) {
    this(context, null);
}

public DragGridView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public DragGridView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    setOnItemLongClickListener(this);
}

手指在Item上長按時

首先在構造器中得到WindowManager對象以及設置長按監聽器,所以只有長按item才能拖拽。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mWindowX = ev.getRawX();
            mWindowY = ev.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return super.onInterceptTouchEvent(ev);
}

然后在onInterceptTouchEvent(MotionEvent ev)中得到手指下落時的ev.getRawX()ev.getRawY(),以備后面的計算使用。至于getRawX()getX()的區別這里就不再講述了,如果有不懂的可以自行百度。

下面就是onItemLongClick(AdapterView<?> parent, View view, int position, long id)方法了,我們在DragGridView中定義了兩種模式:MODE_DRAGMODE_NORMAL,分別對應著item拖拽和item不拖拽:

@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
    if (mode == MODE_DRAG) {
        return false;
    }
    this.view = view;
    this.position = position;
    this.tempPosition = position;
    mX = mWindowX - view.getLeft() - this.getLeft();
    mY = mWindowY - view.getTop() - this.getTop();
    initWindow();
    return true;
}

在onItemLongClick()中先判斷了一下模式,只有在MODE_NORMAL的情況下才會添加window。然后計算出mX和mY??赡苡行┩趍X和mY的計算上看不懂,我給出了一個圖示:

mX、mY示意圖

其中紅點是手指按下的坐標,也就是(mWindowX,mWindowY)這個點;綠邊框為DragGridView,因為DragGridView有可能會有margin值;所以this.getLeft()就是綠邊框到屏幕的距離,而view.getLeft()就是長按的Item的左邊到綠邊框的距離。這幾個值相減就得到了mX。同理,mY也是這樣得到的。

然后來看看initWindow();這個方法:

/**
 * 初始化window
 */
private void initWindow() {
    if (dragView == null) {
        dragView = View.inflate(getContext(), R.layout.drag_item, null);
        TextView tv_text = (TextView) dragView.findViewById(R.id.tv_text);
        tv_text.setText(((TextView) view.findViewById(R.id.tv_text)).getText());
    }
    if (layoutParams == null) {
        layoutParams = new WindowManager.LayoutParams();
        layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        layoutParams.format = PixelFormat.RGBA_8888;
        layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;  //懸浮窗的行為,比如說不可聚焦,非模態對話框等等
        layoutParams.width = view.getWidth();
        layoutParams.height = view.getHeight();
        layoutParams.x = view.getLeft() + this.getLeft();  //懸浮窗X的位置
        layoutParams.y = view.getTop() + this.getTop();  //懸浮窗Y的位置
        view.setVisibility(INVISIBLE);
    }

    mWindowManager.addView(dragView, layoutParams);
    mode = MODE_DRAG;
}

initWindow()中,我們先創建了一個dragView,而dragView里面的內容與長按的Item的內容完全一致。然后創建WindowManager.LayoutParams的對象,把dragView添加到window上去。同時,也要把長按的Item隱藏了。在這里別忘了需要申請顯示懸浮窗的權限:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

手指滑動時

initWindow()之后,我們就要考慮當手指滑動時window也要跟著動了,我們重寫onTouchEvent(MotionEvent ev)來監聽滑動事件,可以看到下面的updateWindow(ev)方法。

@Override
public boolean onTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            break;
        case MotionEvent.ACTION_MOVE:
            if (mode == MODE_DRAG) {
                updateWindow(ev);
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mode == MODE_DRAG) {
                closeWindow(ev.getX(), ev.getY());
            }
            break;
    }
    return super.onTouchEvent(ev);
}

這里貼出updateWindow(ev)方法:

/**
 * 觸摸移動時,window更新
 *
 * @param ev
 */
private void updateWindow(MotionEvent ev) {
    if (mode == MODE_DRAG) {
        float x = ev.getRawX() - mX;
        float y = ev.getRawY() - mY;
        if (layoutParams != null) {
            layoutParams.x = (int) x;
            layoutParams.y = (int) y;
            mWindowManager.updateViewLayout(dragView, layoutParams);
        }
        float mx = ev.getX();
        float my = ev.getY();
        int dropPosition = pointToPosition((int) mx, (int) my);
        Log.i(TAG, "dropPosition : " + dropPosition + " , tempPosition : " + tempPosition);
        if (dropPosition == tempPosition || dropPosition == GridView.INVALID_POSITION) {
            return;
        }
        itemMove(dropPosition);
    }
}

在這里,mX和mY就派上用場了。根據ev.getRawX()ev.getRawY()分別減去mXmY就得到了移動中layoutParams.x和layoutParams.y。再調用updateViewLayout (View view, ViewGroup.LayoutParams params)就出現了window跟隨手指滑動而滑動的效果。最后根據 pointToPosition(int x, int y)返回的值來執行itemMove(dropPosition);。

/**
 * 判斷item移動,作出移動動畫
 *
 * @param dropPosition
 */
private void itemMove(int dropPosition) {
    TranslateAnimation translateAnimation;
    // 移動的位置在原位置前面時
    if (dropPosition < tempPosition) {
        for (int i = dropPosition; i < tempPosition; i++) {
            View view = getChildAt(i);
            View nextView = getChildAt(i + 1);
            float xValue = (nextView.getLeft() - view.getLeft()) * 1f / view.getWidth();
            float yValue = (nextView.getTop() - view.getTop()) * 1f / view.getHeight();
            translateAnimation =
                    new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, xValue, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, yValue);
            translateAnimation.setInterpolator(new LinearInterpolator());
            translateAnimation.setFillAfter(true);
            translateAnimation.setDuration(300);
            if (i == tempPosition - 1) {
                translateAnimation.setAnimationListener(animationListener);
            }
            view.startAnimation(translateAnimation);
        }
    } else {
        // 移動的位置在原位置后面時
        for (int i = tempPosition + 1; i <= dropPosition; i++) {
            View view = getChildAt(i);
            View prevView = getChildAt(i - 1);
            float xValue = (prevView.getLeft() - view.getLeft()) * 1f / view.getWidth();
            float yValue = (prevView.getTop() - view.getTop()) * 1f / view.getHeight();
            translateAnimation =
                    new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, xValue, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, yValue);
            translateAnimation.setInterpolator(new LinearInterpolator());
            translateAnimation.setFillAfter(true);
            translateAnimation.setDuration(300);
            if (i == dropPosition) {
                translateAnimation.setAnimationListener(animationListener);
            }
            view.startAnimation(translateAnimation);
        }
    }
    tempPosition = dropPosition;
}

/**
 * 動畫監聽器
 */
Animation.AnimationListener animationListener = new Animation.AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) {

    }

    @Override
    public void onAnimationEnd(Animation animation) {
        // 在動畫完成時將adapter里的數據交換位置
        ListAdapter adapter = getAdapter();
        if (adapter != null && adapter instanceof DragGridAdapter) {
            ((DragGridAdapter) adapter).exchangePosition(position, tempPosition, true);
        }
        position = tempPosition;
    }

    @Override
    public void onAnimationRepeat(Animation animation) {

    }
};

上面的代碼主要是根據dropPosition使要改變位置的Item來做出平移動畫,當最后一個要改變位置的Item平移動畫完成之后,在adapter中完成數據集的交換。

/**
 * 給item交換位置
 *
 * @param originalPosition item原先位置
 * @param nowPosition      item現在位置
 */
public void exchangePosition(int originalPosition, int nowPosition, boolean isMove) {
    T t = list.get(originalPosition);
    list.remove(originalPosition);
    list.add(nowPosition, t);
    movePosition = nowPosition;
    this.isMove = isMove;
    notifyDataSetChanged();
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    Log.i(TAG, "-------------------------------");
    for (T t : list){
        Log.i(TAG, t.toString());
    }
    View view = getItemView(position, convertView, parent);
    if (position == movePosition && isMove) {
        view.setVisibility(View.INVISIBLE);
    }
    return view;
}

手指抬起時

在上面onTouchEvent(MotionEvent ev)方法中,可以看到手指抬起時調用了closeWindow(ev.getX(), ev.getY());,那就一起來看看:

/**
 * 關閉window
 *
 * @param x
 * @param y
 */
private void closeWindow(float x, float y) {
    if (dragView != null) {
        mWindowManager.removeView(dragView);
        dragView = null;
        layoutParams = null;
    }
    itemDrop();
    mode = MODE_NORMAL;
}

/**
 * 手指抬起時,item下落
 */
private void itemDrop() {
    if (tempPosition == position || tempPosition == GridView.INVALID_POSITION) {
        getChildAt(position).setVisibility(VISIBLE);
    } else {
        ListAdapter adapter = getAdapter();
        if (adapter != null && adapter instanceof DragGridAdapter) {
            ((DragGridAdapter) adapter).exchangePosition(position, tempPosition, false);
        }
    }
}

可以看出主要做的事情就是移除了window,并且也是調用了exchangePosition(int originalPosition, int nowPosition, boolean isMove),不同的是第三個參數isMove傳入了false,這樣所有的Item都顯示出來了。

講了這么多,來看看最后的效果吧:

最終效果圖gif

和網易新聞的效果不相上下吧,完整的源碼太長就不貼出了,下面提供源碼下載:

DragGridView.rar

GitHub:

DragGridView

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容