如何成為自定義高手(六)滑動(dòng)和拖拽

滑動(dòng)

GestureDetector

GestureDetector手勢(shì)檢測(cè):常用用來(lái)檢測(cè)onSingleTapUp(單擊),onFling(快速滑動(dòng)),onScroll(拖動(dòng)),onLongPress(長(zhǎng)按),onDoubleTap(雙擊)

VelocityTracker

速度追蹤器,就是用來(lái)計(jì)算手指的滑動(dòng)速度
使用方法:

  • ACTION_DOWN 事件到來(lái)時(shí),通過(guò) VelocityTracker.obtain()創(chuàng)建?個(gè)實(shí)例,或者使用 velocityTracker.clear() 把之前的某個(gè)實(shí)例重置
  • 對(duì)于每個(gè)事件(包括 ACTION_DOWN 事件),使用velocityTracker.addMovement(event) 把事件添加進(jìn) VelocityTracker
  • 在需要速度的時(shí)候(例如在 ACTION_UP 中計(jì)算是否達(dá)到 fling 速度),使用velocityTracker.computeCurrentVelocity(1000, maxVelocity) 來(lái)計(jì)算實(shí)時(shí)速度,并通過(guò)getXVelocity() / getYVelocity() 來(lái)獲取計(jì)算出的速度。
    方法參數(shù)中的 1000 是指的計(jì)算的時(shí)間長(zhǎng)度,單位是 ms。例如這?填入 1000,那么getXVelocity() 返回的值就是每 1000ms (即?秒)時(shí)間內(nèi)手指移動(dòng)的像素?cái)?shù)。第?個(gè)參數(shù)是速度上限,超過(guò)這個(gè)速度時(shí),計(jì)算出的速度會(huì)回落到這個(gè)速度。例如這里填了 200,而實(shí)時(shí)速度是 300,那么實(shí)際的返回速度將是 200 ,maxVelocity 可以通過(guò) viewConfiguration.getScaledMaximumFlingVelocity()來(lái)獲取。
    VelocityTracker velocityTracker = VelocityTracker.obtain();
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            velocityTracker.clear();
        }
        velocityTracker.addMovement(event);

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                velocityTracker.computeCurrentVelocity(1000, maxVelocity);           
                break;
        }
        return true;
    }

scrollTo,scrollBy,computeScroll

scrollTo(x, y)移動(dòng)的是絕對(duì)值;scrollBy(deltaX, deltaY)移動(dòng)的是相對(duì)值,內(nèi)部也是調(diào)用scrollTo方法。
scrollTo() 是瞬時(shí)方法,不會(huì)自動(dòng)使用動(dòng)畫(huà)。如果要用動(dòng)畫(huà),需要配合 View.computeScroll()方法。computeScroll() 在 View 重繪時(shí)被自動(dòng)調(diào)用

使用OverScroller實(shí)現(xiàn)緩慢滑動(dòng)

// onTouchEvent() 中:
overScroller.startScroll(startX, startY, dx, dy);
postInvalidateOnAnimation();
......
// onTouchEvent() 外:
@Override
public void computeScroll() {
   if (overScroller.computeScrollOffset()) { // 計(jì)算實(shí)時(shí)位置
      scrollTo(overScroller.getCurrX(),
      overScroller.getCurrY()); // 更新界?
      postInvalidateOnAnimation(); // 下?幀繼續(xù)
   }
}

使用Scroller實(shí)現(xiàn)緩慢滑動(dòng)
實(shí)現(xiàn)原理:startScroll記錄下相關(guān)參數(shù),invalidate導(dǎo)致view重繪,view的draw方法中又調(diào)用computeScroll,而computeScroll又會(huì)向Scroller獲取當(dāng)前scrollX和scrollY,然后通過(guò)scrollTo去實(shí)現(xiàn)滑動(dòng)

private void smoothScrollTo(int destX, int destY){
    int scrollX = getScrollX();
    int deltaX = destX - scrollX;
    int scrollY = getScrollY();
    int deltaY = destY - scrollY;
    //1000ms內(nèi)滑向destX,效果就是慢慢滑動(dòng)
    mScroller.startScroll(scrollX,scrollY,deltaX,deltaY,1000);
    invalidate();
}

@Override
public void computeScroll() {
    if(mScroller.computeScrollOffset()){
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        postInvalidate();
    }
    super.computeScroll();
}

簡(jiǎn)單的自定義ViewPager

通過(guò)簡(jiǎn)單的自定義ViewPager來(lái)使用上面的VelocityTracker和computeScroll等使用

public class MyViewPager extends ViewGroup {
    float downX;
    float downY;
    float downScrollX;
    boolean scrolling;
    float minVelocity;
    float maxVelocity;
    OverScroller overScroller;
    ViewConfiguration viewConfiguration;
    VelocityTracker velocityTracker = VelocityTracker.obtain();

    public MyViewPager (Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        overScroller = new OverScroller(context);
        viewConfiguration = ViewConfiguration.get(context);
        maxVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
        minVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        int childTop = 0;
        int childRight = getWidth();
        int childBottom = getHeight();
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.layout(childLeft, childTop, childRight, childBottom);
            childLeft += getWidth();
            childRight += getWidth();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
            velocityTracker.clear();
        }
        velocityTracker.addMovement(ev);

        boolean result = false;
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                scrolling = false;
                downX = ev.getX();
                downY = ev.getY();
                downScrollX = getScrollX();
                break;
            case MotionEvent.ACTION_MOVE:
                float dx = downX - ev.getX();
                if (!scrolling) {
                    if (Math.abs(dx) > viewConfiguration.getScaledPagingTouchSlop()) {
                        scrolling = true;
                        getParent().requestDisallowInterceptTouchEvent(true);
                        result = true;
                    }
                }
                break;
        }
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            velocityTracker.clear();
        }
        velocityTracker.addMovement(event);

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                downY = event.getY();
                downScrollX = getScrollX();
                break;
            case MotionEvent.ACTION_MOVE:
                float dx = downX - event.getX() + downScrollX;
                if (dx > getWidth()) {
                    dx = getWidth();
                } else if (dx < 0) {
                    dx = 0;
                }
                scrollTo((int) (dx), 0);
                break;
            case MotionEvent.ACTION_UP:
                velocityTracker.computeCurrentVelocity(1000, maxVelocity);
                float vx = velocityTracker.getXVelocity();
                int scrollX = getScrollX();
                int targetPage;
                if (Math.abs(vx) < minVelocity) {
                    targetPage = scrollX > getWidth() / 2 ? 1 : 0;
                } else {
                    targetPage = vx < 0 ? 1 : 0;
                }
                int scrollDistance = targetPage == 1 ? (getWidth() - scrollX) : - scrollX;
                overScroller.startScroll(getScrollX(), 0, scrollDistance, 0);
                postInvalidateOnAnimation();
                break;
        }
        return true;
    }

    @Override
    public void computeScroll() {
        if (overScroller.computeScrollOffset()) {
            scrollTo(overScroller.getCurrX(), overScroller.getCurrY());
            postInvalidateOnAnimation();
        }
    }
}

拖拽

OnDragListener

  • 通過(guò) startDrag() 來(lái)啟動(dòng)拖拽
    startDrag最后會(huì)調(diào)用startDragAndDrop,
startDragAndDrop(ClipData data, DragShadowBuilder shadowBuilder,Object myLocalState, int flags)

內(nèi)部有四個(gè)參數(shù):

  1. ClipData data
    其實(shí)就是一個(gè)封裝數(shù)據(jù)的對(duì)象,通過(guò)拖放操作傳遞給接受者。該對(duì)象可以存放一個(gè)Item的集合,Item可以存放如下數(shù)據(jù):
public static class Item {
        final CharSequence mText;
        final String mHtmlText;
        final Intent mIntent;
        Uri mUri;
}
  1. DragShadowBuilder shadowBuilder
    用于創(chuàng)建拖拽view是的陰影,也就是跟隨手指移動(dòng)的視圖,通常直接使用默認(rèn)即可生成與一個(gè)原始view相同,帶有透明度的陰影
  2. Object myLocalState
    當(dāng)你的拖拽行為是在同一個(gè)Activity中進(jìn)行時(shí)可以傳遞一個(gè)任意對(duì)象,在監(jiān)聽(tīng)中可以通過(guò){@link android.view.DragEvent#getLocalState()}獲得。如果是跨Activity拖拽中無(wú)法訪問(wèn)此數(shù)據(jù),getLocalState()將返回null。
  3. int flags
    控制拖放操作的標(biāo)志。因?yàn)闆](méi)有標(biāo)志可以設(shè)置為0,flag標(biāo)志拖動(dòng)是否可以跨越窗口以及一些訪問(wèn)權(quán)限(需要API24+)
            child.setOnLongClickListener(new OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    draggedView = v;
                    v.startDrag(null, new DragShadowBuilder(v), v, 0);
                    return false;
                }
            });
            child.setOnDragListener(dragListener);
  • 用setOnDragListener() 來(lái)監(jiān)聽(tīng)
    目標(biāo)View:不是被拖拽的View,是要拖拽去哪個(gè)區(qū)域,這個(gè)區(qū)域就目標(biāo)View,它要設(shè)置OnDragListener監(jiān)聽(tīng)。
    OnDragListener 內(nèi)部只有?個(gè)方法: onDrag()。View中onDragEvent() 方法也會(huì)收到拖拽回調(diào)(界?中的每個(gè) View 都會(huì)收到)
view.setOnDragListener(new View.OnDragListener() {
            @Override
            public boolean onDrag(View v, DragEvent event) {
                //v 永遠(yuǎn)是設(shè)置該監(jiān)聽(tīng)的view,這里即fl_blue
                String simpleName = v.getClass().getSimpleName();
                Log.w(BLUE, "view name:" + simpleName);
                //獲取事件
                int action = event.getAction();
                switch (action) {
                    case DragEvent.ACTION_DRAG_STARTED:
                        Log.i(BLUE, "開(kāi)始拖拽");
                        break;
                    case DragEvent.ACTION_DRAG_ENDED:
                        Log.i(BLUE, "結(jié)束拖拽");
                        break;
                    case DragEvent.ACTION_DRAG_ENTERED:
                        Log.i(BLUE, "拖拽的view進(jìn)入監(jiān)聽(tīng)的view時(shí)");
                        break;
                    case DragEvent.ACTION_DRAG_EXITED:
                        Log.i(BLUE, "拖拽的view離開(kāi)監(jiān)聽(tīng)的view時(shí)");
                        break;
                    case DragEvent.ACTION_DRAG_LOCATION:
                        float x = event.getX();
                        float y = event.getY();
                        long l = SystemClock.currentThreadTimeMillis();
                        Log.i(BLUE, "拖拽的view在監(jiān)聽(tīng)view中的位置:x =" + x + ",y=" + y);
                        break;
                    case DragEvent.ACTION_DROP:
                        Log.i(BLUE, "釋放拖拽的view");
                        break;
                }
                //是否響應(yīng)拖拽事件,true響應(yīng),返回false只能接受到ACTION_DRAG_STARTED事件,后續(xù)事件不會(huì)收到
                return true;
            }
        });

ViewDragHelper

  • 需要?jiǎng)?chuàng)建?個(gè) ViewDragHelper 和 Callback()
    ViewDragHelper create(ViewGroup forParent, Callback cb);一個(gè)靜態(tài)的創(chuàng)建方法,
    參數(shù)1:出入的是相應(yīng)的ViewGroup
    參數(shù)2:是一個(gè)回調(diào)Callback,在后面介紹包括其中的方法
    ViewDragHelper dragHelper;
    ViewDragHelper.Callback dragListener = new DragListener();

    public DragHelperLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        dragHelper = ViewDragHelper.create(this, dragListener);
        viewConfiguration = ViewConfiguration.get(context);
    }
  • 需要寫(xiě)在 ViewGroup 里面,重寫(xiě) onIntercept() 和 onTouchevent()
  1. shouldInterceptTouchEvent(MotionEvent ev) 處理事件分發(fā)的(主要是將ViewGroup的事件攔截onInterceptTouchEvent,委托給ViewDragHelper進(jìn)行處理)
  2. processTouchEvent(MotionEvent event) 處理相應(yīng)TouchEvent的方法,這里要注意一個(gè)問(wèn)題,處理相應(yīng)的TouchEvent的時(shí)候要將結(jié)果返回為true,消費(fèi)本次事件!否則將無(wú)法使用ViewDragHelper處理相應(yīng)的拖拽事件!
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return dragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        dragHelper.processTouchEvent(event);
        return true;
    }
  • ViewDragHelper.Callback的API(也就是創(chuàng)建ViewDragHelper傳入的回調(diào)方法)
  1. tryCaptureView(View child, int pointerId) 這是一個(gè)抽象類(lèi),必須去實(shí)現(xiàn),也只有在這個(gè)方法返回true的時(shí)候下面的方法才會(huì)生效;相當(dāng)于事件的開(kāi)始
    參數(shù)1:捕獲的View(也就是你拖動(dòng)的這個(gè)View)
    參數(shù)2:這個(gè)參數(shù)我也不知道什么意思API中寫(xiě)的一個(gè)什么指針,這里沒(méi)有到也沒(méi)有注意
  2. onViewDragStateChanged(int state) 當(dāng)狀態(tài)改變的時(shí)候回調(diào),返回相應(yīng)的狀態(tài)(這里有三種狀態(tài))
    STATE_IDLE 閑置狀態(tài)
    STATE_DRAGGING 正在拖動(dòng)
    STATE_SETTLING 放置到某個(gè)位置
  3. onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 當(dāng)你拖動(dòng)的View位置發(fā)生改變的時(shí)候回調(diào)
    參數(shù)1:你當(dāng)前拖動(dòng)的這個(gè)View
    參數(shù)2:距離左邊的距離
    參數(shù)3:距離右邊的距離
    參數(shù)4:x軸的變化量
    參數(shù)5:y軸的變化量
  4. onViewCaptured(View capturedChild, int activePointerId)捕獲View的時(shí)候調(diào)用的方法
    參數(shù)1:捕獲的View(也就是你拖動(dòng)的這個(gè)View)
    參數(shù)2:這個(gè)參數(shù)我也不知道什么意思API中寫(xiě)的一個(gè)什么指針,這里沒(méi)有到也沒(méi)有注意
  5. onViewReleased(View releasedChild, float xvel, float yvel) 當(dāng)View停止拖拽的時(shí)候調(diào)用的方法,一般在這個(gè)方法中重置一些參數(shù),相當(dāng)于事件的結(jié)束
    參數(shù)1:你拖拽的這個(gè)View
    參數(shù)2:x軸的速率
    參數(shù)3:y軸的速率
  6. clampViewPositionVertical(View child, int top, int dy) 豎直拖拽的時(shí)候回調(diào)的方法
    參數(shù)1:拖拽的View
    參數(shù)2:距離頂部的距離
    參數(shù)3:變化量
    7.clampViewPositionHorizontal(View child, int left, int dx) 水平拖拽的時(shí)候回調(diào)的方法
    參數(shù)1:拖拽的View
    參數(shù)2:距離左邊的距離
    參數(shù)3:變化量
public class DragHelperLayout extends FrameLayout {

    @Override
    public void computeScroll() {
        if (dragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    private class DragCallback extends ViewDragHelper.Callback {
        float capturedOldLeft;
        float capturedOldTop;

        @Override
        public boolean tryCaptureView(@NonNull View child, int pointerId) {
            return true;
        }

        @Override
        public void onViewDragStateChanged(int state) {
            if (state == ViewDragHelper.STATE_IDLE) {
                View capturedView = dragHelper.getCapturedView();
                //。。。。
            }
        }

        @Override
        public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
            return left;
        }

        @Override
        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
            return top;
        }

        @Override
        public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
            capturedOldLeft = capturedChild.getLeft();
            capturedOldTop = capturedChild.getTop();
        }

        @Override
        public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
        }

        @Override
        public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
            //放回到原來(lái)的位置m
            dragHelper.settleCapturedViewAt((int) capturedOldLeft, (int) capturedOldTop);
            postInvalidateOnAnimation();
        }
    }
}

如何成為自定義高手(一)繪制
如何成為自定義高手(二)動(dòng)畫(huà)
如何成為自定義高手(三)布局
如何成為自定義高手(四)觸摸反饋,事件分發(fā)機(jī)制
如何成為自定義高手(五)多點(diǎn)觸摸
如何成為自定義高手(六)滑動(dòng)和拖拽
如何成為自定義高手(七)滑動(dòng)沖突

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容