欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android自定義ViewGroup實現(xiàn)側滑菜單

 更新時間:2023年01月05日 08:36:44   作者:newki  
這篇文章主要為大家詳細介紹了Android如何通過自定義ViewGroup實現(xiàn)側滑菜單,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下

前言

前文我們理解了ViewGroup的測量與布局,但是并沒有涉及到多少的交互邏輯,而 ViewGroup 的交互邏輯說起來范圍其實是比較大的。從哪開始說起呢?

我們暫且把 ViewGroup 的交互分為幾塊知識區(qū),

  • 事件的攔截。
  • 事件的處理(內部又分不同的處理方式)。
  • 子View的移動與協(xié)調。
  • 父ViewGroup的協(xié)調運動。

然后我們先簡單的做一個介紹,需要注意的是下面每一種方式單獨拿出來都是一個知識點或知識面,這里我個人理解的話,可以當做一個目錄,我們先簡單的復習學習一下,心里過一遍,如果遇到哪一個知識點不是那么了解,那我們也可以單獨的對這個技術點進行搜索與對應的學習。

而本文介紹完目錄之后,我們會針對其中的一種【子View的協(xié)調運動】,也就是本文的側滑菜單效果做講解,后期也會對一些其他常用的效果再做分析哦。

話不多說,Let's go

一、常用的幾種交互方式

一般來說,常見的幾種場景通常來說涉及到如下的幾種方式。每一種方式又根據(jù)不同的效果可以分為不同的方式來實現(xiàn)。

需要注意的是有時候也并非唯一解,也可以通過不同的方式實現(xiàn)同樣的效果。也可以通過不同的方式組合起來,實現(xiàn)一些特定的效果。

下面我們先從事件的分發(fā)與攔截說起:

1.1 事件的攔截處理

自定義 ViewGroup 的一種分類,還比較常用的就是解決事件的沖突,常用的就是事件的攔截,這一點就需要了解一點 View 的事件分發(fā)與攔截的機制了。不過相信大家多多少少都懂一點,畢竟也是面試必出題了,下面簡單說一下。

事件分發(fā)方面的區(qū)別:

事件分發(fā)機制主要有三個方法:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()

ViewGroup包含這三個方法,而View則只包含dispatchTouchEvent()、onTouchEvent()兩個方法,不包含onInterceptTouchEvent()。

onTouchEvent() 與 dispatchTouchEvent() 相信大家都有所了解。

onTouchEvent() 是事件的響應與處理,而dispatchTouchEvent() 是事件的分發(fā)。

需要注意的是當某個子View的dispatchTouchEvent()返回true時,會中止Down事件的分發(fā),同時在ViewGroup中記錄該子View。接下來的Move和Up事件將由該子View直接進行處理。

而 onInterceptTouchEvent() 就是ViewGroup專有的攔截處理,雖然子 View 沒有攔截的方法,但是子View可以通過調用方法 getParent().requestDisallowInterceptTouchEvent() 請求父ViewGroup不攔截事件。

通過 重寫 onInterceptTouchEvent() 或者 使用 requestDisallowInterceptTouchEvent() 即可達到事件攔截的處理。

關于事件的處理這里可以引用一張圖,非常的清晰:

實際的應用,我這里以 ViewPager2 嵌套 RecyclerView 的場景為例。

如圖所示的分類列表,我們可以使用ViewPager2 嵌套 RV 來實現(xiàn)。(具體的實現(xiàn)方式有多種,這里不做討論),那么就會出現(xiàn)一個問題。什么時候滾動子 RV 。什么時候滾動垂直的父 VP2 。如果大家有嘗試過類似的場景,相信大家就能理解這其中的坑點,隨機出現(xiàn)父布局與子布局的滾動,也就是說有還是有事件沖突的問題。

就算大家使用別的方案解決了這個問題,那么換成一個復雜的分類列表又如何?

再比如這種復雜的分類頁面,由于數(shù)據(jù)量比較大,子 RV 的上拉滑動事件中還需要加入上拉加載的時間。這一個分類滑動完畢之后,還需要切換右上的橫向Tab。當橫向Tab到最后一個了,并且滑動完畢之后,左側的滾動Tab才往下走一個。

面對如此復雜的分類列表滾動邏輯,我們就需要使用自定義ViewGroup時間攔截層,自己控制什么時機由子 RV 控制滑動,什么時機由父 VP2 控制滑動。

這里我們以上圖的簡單示例為主,也是默認的常用效果,當子 RV 滾動完成之后再交由父 VP2 滾動。我們定義的攔截層自定義ViewGroup如下:

class NestedScrollableHost : FrameLayout {

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    private var touchSlop = 0
    private var initialX = 0f
    private var initialY = 0f

    private val parentViewPager: ViewPager2?
        get() {
            var v: View? = parent as? View
            while (v != null && v !is ViewPager2) {
                v = v.parent as? View
            }
            return v as? ViewPager2
        }

    private val child: View? get() = if (childCount > 0) getChildAt(0) else null

    init {
        touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    }

    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
        val direction = -delta.sign.toInt()
        return when (orientation) {
            0 -> child?.canScrollHorizontally(direction) ?: false
            1 -> child?.canScrollVertically(direction) ?: false
            else -> throw IllegalArgumentException()
        }
    }

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        return handleInterceptTouchEvent(e)
    }


    private fun handleInterceptTouchEvent(e: MotionEvent): Boolean {

        val orientation = parentViewPager?.orientation ?: return false

        if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
            return false
        }

        if (e.action == MotionEvent.ACTION_DOWN) {
            initialX = e.x
            initialY = e.y
            parent.requestDisallowInterceptTouchEvent(true)

        } else if (e.action == MotionEvent.ACTION_MOVE) {

            val dx = e.x - initialX
            val dy = e.y - initialY
            val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL

            val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
            val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f

            if (scaledDx > touchSlop || scaledDy > touchSlop) {
                return if (isVpHorizontal == (scaledDy > scaledDx)) {
                    //垂直的手勢攔截
                    parent.requestDisallowInterceptTouchEvent(false)
                    true
                } else {

                    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
                        //子View能滾動,不攔截事件
                        parent.requestDisallowInterceptTouchEvent(true)
                        false
                    } else {
                        //子View不能滾動,直接就攔截事件
                        parent.requestDisallowInterceptTouchEvent(false)
                        true
                    }
                }
            }
        }

        return false
    }

}

這里主要的邏輯就是對攔截做處理,而如果是下圖中復雜的分類頁面,也是類似的邏輯,只是需要手動的控制是否攔截了。可以實現(xiàn)同樣的效果的。

而除了攔截事件的自定義 ViewGroup 的場景之外,我們用的比較多的就是事件的處理了,事件的處理又分很多,可以自己手撕 onTouchEvent 。也可通過 Scroller 來實現(xiàn)滾動效果。也能通過 GestureDetector 手勢識別器來幫我們完成。

下面一起來看看分別如何實現(xiàn):

1.2 自行處理事件的幾種方式

在之前的 View 和 ViewGroup 的學習中,我們一般都是自己來處理事件的響應與攔截,一般都是通過 MotionEvent 對象,拿到它的事件和一些位置信息,做繪制和事件攔截。

其實除了這一種最基本的方式,還有其他的方式也同樣可以操作,分為不同的場景,我們可以選擇性的使用不同的方式,都可以達到同樣的效果。

onTouchEvent

我們比較常見的就是在 dispatchTouchEvent()、onTouchEvent() 兩個方法中通過 MotionEvent 對象來操作屬性。

比較常用的就是通過手勢記錄坐標點,然后進行繪制,或者進行事件的攔截。

例如,如果想繪制,我們可以記錄變化的X與Y,然后通過指定的公式轉換為繪制的變量,然后通過 invalidate 觸發(fā)重繪,在 onDraw 中取到變化的變量繪制出來,達到動畫或滾動或其他的一些效果。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {

            //按下的時候記錄當前操作的是左側限制圓還是右側的限制圓
            downX = event.getX();
            touchLeftCircle = checkTouchCircleLeftOrRight(downX);

            if (touchLeftCircle) {
                //如果是左側
                //如果超過右側最大值則不處理
                if (downX + perSlice > mRightCircleCenterX) {
                    return false;
                }

                mLeftCircleCenterX = downX;
            } else {
                //如果是右側
                //如果超過左側最小值則不處理
                if (downX - perSlice < mLeftCircleCenterX) {
                    return false;
                }

                mRightCircleCenterX = downX;
            }

        } 

        //中間的進度矩形是根據(jù)兩邊圓心點動態(tài)計算的
        mSelectedCornerLineRect.left = mLeftCircleCenterX;
        mSelectedCornerLineRect.right = mRightCircleCenterX;

        //全部的事件處理完畢,變量賦值完成之后,開始重繪
        invalidate();

        return true;
    }

或者我們可以通過記錄X和Y的坐標,判斷滑動的方向從而進行事件的攔截:

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getRawX();
        int y = (int) ev.getRawY();
        int dealtX = 0;
        int dealtY = 0;

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                dealtX = 0;
                dealtY = 0;
                // 保證子View能夠接收到Action_move事件
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                dealtX += Math.abs(x - lastX);
                dealtY += Math.abs(y - lastY);
 
                // 這里是否攔截的判斷依據(jù)是左右滑動,讀者可根據(jù)自己的邏輯進行是否攔截
                if (dealtX >= dealtY) { // 左右滑動請求父 View 不要攔截
                    getParent().requestDisallowInterceptTouchEvent(true);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
            case MotionEvent.ACTION_UP:
                break;

        }
        return super.dispatchTouchEvent(ev);
    }

這種方式相信也是大家見的最多的,看見代碼就知道是什么意思,所以這里就不放圖與Demo了,如果想了解,也可以看看我之前的自定義View繪制文章,基本都是這個套路。

接下來我們繼續(xù),那么除了原始的 MotionEvent 做移動之外,我們甚至可以使用 Scroller 來專門做滾動的操作。只是相對來說 Scroller 是比較少用的。(畢竟谷歌給我們的太多的滾動的控件了),但是掌握之后可以實現(xiàn)一些特殊的效果,也是值得一學,下面一起看看吧。

Scroller

Scroller 譯為滾動器,是 ViewGroup 類中原生支持的一個功能。Scroller 類并不負責滾動這個動作,只是根據(jù)要滾動的起始位置和結束位置生成中間的過渡位置,從而形成一個滾動的動畫。

Scroller 本身并不神秘與復雜,它只是模擬提供了滾動時相應數(shù)值的變化,復寫自定義 View 中的 computeScroll() 方法,在這里獲取 Scroller 中的 mCurrentX 和 mCurrentY,根據(jù)自己的規(guī)則調用 scrollTo() 方法,就可以達到平穩(wěn)滾動的效果。

本質上就是一個持續(xù)不斷刷新 View 的繪圖區(qū)域的過程,給定一個起始位置、結束位置、滾動的持續(xù)時間,Scroller 自動計算出中間位置和滾動節(jié)奏,再調用 invalidate()方法不斷刷新。

需要注意的是調用scrollTo()和 scrollBy()的區(qū)別。其實也不復雜,我們翻譯為中文的意思,scrollTo是滾動到xx,scrollBy是滾動了xx,這樣是不是就一下就理解了。

剩下的就是需要重寫computeScroll執(zhí)行滾動的邏輯。

下面舉個簡單的栗子:

我們使用 Scroller模仿一個 簡易的 ViewPager 效果。自定義ViewGroup中加入了9個View。并且占滿全屏,然后我們上滑動切換布局,當停手會判斷是回到當前View還是去下一個View。

ViewGroup的測量與布局在之前的文章中我們已經反復的復習了,這應該沒什么問題:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        //設置ViewGroup的高度
        MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
        mlp.height = mScreenHeight * childCount;
        setLayoutParams(mlp);

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);

            if (child.getVisibility() != View.GONE) {
                child.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight);
            }
        }
    }

然后就是對Touch和滾動的操作:

    private int mLastY;
    private int mStart;
    private int mEnd;
    private Scroller mScroller;

    ...

    @Override
    public void computeScroll() {
        super.computeScroll();

        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            postInvalidate();
        }
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                mStart = getScrollY();
                break;
            case MotionEvent.ACTION_MOVE:
                //當停止動畫的時候,它會馬上滾動到終點,然后向動畫設置為結束。
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                int dy = mLastY - y;
                if (getScrollY() < 0) {
                    dy = 0;
                }
                //開始滾動
                scrollBy(0, dy);
                mLastY = y;
                break;
            case MotionEvent.ACTION_UP:
                mEnd = getScrollY();
                int dScrollY = mEnd - mStart;
                if (dScrollY > 0) {
                    if (dScrollY < mScreenHeight / 3) {
                        mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
                    } else {
                        mScroller.startScroll(0, getScrollY(), 0, mScreenHeight - dScrollY);
                    }
                } else {
                    if (-dScrollY < mScreenHeight / 3) {
                        mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
                    } else {
                        mScroller.startScroll(0, getScrollY(), 0, -mScreenHeight - dScrollY);
                    }
                }
                invalidate();
                break;

        }

        return true;
    }

那么實現(xiàn)的效果就是如下圖所示:

是不是相當于一個簡配的ViewPager呢。。。

既然我們的一些事件點擊和移動可以通過 MotionEvent 來實現(xiàn),一些特定的滾動效果還能通過 Scroller 來實現(xiàn)。有沒有更方便的一種方式全部幫我們實現(xiàn)呢?

接下來就是我們常用的 GestureDetector 類了??梢詭椭覀兛焖賹崿F(xiàn)點擊與滾動效果。

GestureDetector

GestureDetector類,這個類指明是手勢識別器,它內部封裝了一些常用的手勢操作的接口,讓我們快速的處理手勢事件,比如單機、雙擊、長按、滾動等。

通常來說我們使用 GestureDetector 分為三步:

  • 初始化 GestureDetector 類。
  • 定義自己的監(jiān)聽類OnGestureListener,例如實現(xiàn) GestureDetector.SimpleOnGestureListener。
  • 在 dispatchTouchEvent 或 onTouchEvent 方法中,通過GestureDetector將 MotionEvent 事件交給監(jiān)聽器 OnGestureListener

例如我們最簡單的例子自定義View,控制View跟隨手指移動,我們之前的做法是手撕 onTouchEvent,在按下的時候記錄坐標,移動的時候計算坐標,然后重繪達到View跟隨手指移動的效果。那么此時我們就能使用另一種方式來實現(xiàn):

  private GestureDetector mGestureDetector;
  private float centerX;
  private float centerY;

  private void init(Context context) {
        mGestureDetector = new GestureDetector(context, new MTouchDetector());
        setClickable(true);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        //將Event事件交給監(jiān)聽器 OnGestureListener
        mGestureDetector.onTouchEvent(event);

        return super.dispatchTouchEvent(event);
    }

      private class MTouchDetector extends GestureDetector.SimpleOnGestureListener {

        public boolean onDown(MotionEvent e) {
            YYLogUtils.w("MTouchDetector-onDown");
            return super.onDown(e);
        }

        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
           
            centerY -= distanceY;
            centerX -= distanceX;

            //邊界處理 ...

             postInvalidate();
        }

    }

上面我們通過 GestureDetector 來實現(xiàn)了 onTouch 中的繪制效果,那么同樣的我們也可以通過 GestureDetector 來實現(xiàn) onTouch 中的時間攔截效果:

  private GestureDetector mGestureDetector;

  private void init(Context context) {
        mGestureDetector = new GestureDetector(context, new MTouchDetector());
        setClickable(true);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        // 先告訴父Viewgroup,不要攔截,然后再內部判斷是否攔截
        getParent().requestDisallowInterceptTouchEvent(true);
        //將Event事件交給監(jiān)聽器 OnGestureListener
        mGestureDetector.onTouchEvent(event);

        return super.dispatchTouchEvent(event);
    }

      private class MTouchDetector extends GestureDetector.SimpleOnGestureListener {

        public boolean onDown(MotionEvent e) {
            YYLogUtils.w("MTouchDetector-onDown");
            return super.onDown(e);
        }

        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
           
         if (1.732 * Math.abs(distanceX) >= Math.abs(distanceY)) {

                YYLogUtils.w("請求不要攔截我");
                getParent().requestDisallowInterceptTouchEvent(true);
                return true;

            } else {
                YYLogUtils.w("攔截我");
                getParent().requestDisallowInterceptTouchEvent(false);
                return false;
            }
        }
        ...
    }

GestureDetector 甚至能實現(xiàn) Scroller 的效果,實現(xiàn)山寨ViewPager的效果,

  private GestureDetector mGestureDetector;

  private void init(Context context) {
        mGestureDetector = new GestureDetector(context, new MTouchDetector());
        setClickable(true);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        //將Event事件交給監(jiān)聽器 OnGestureListener
        mGestureDetector.onTouchEvent(event);

        return super.dispatchTouchEvent(event);
    }

      private class MTouchDetector extends GestureDetector.SimpleOnGestureListener {

        public boolean onDown(MotionEvent e) {
            YYLogUtils.w("MTouchDetector-onDown");
            return super.onDown(e);
        }

        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
          
           //直接移動
           scrollBy((int) distanceX, getScrollY());

        }
        ...
    }

可以看到我們直接在 GestureDetector 的 onScroll 回調中直接 scrollBy 有上面那種 Scroller 的效果了,比較跟手但是不能指定跳轉到頁面,但是如果想要更好的ViewPager效果,我們需要結合 Scroller 配合的使用就可以有更好的效果。

  private GestureDetector mGestureDetector;
  private int currentIndex;
  private int startX;
  private int endX;
  private Scroller mScroller;

  private void init(Context context) {
        mGestureDetector = new GestureDetector(context, new MTouchDetector());
        setClickable(true);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = (int) event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                endX = (int) event.getX();
 
                int tempIndex = currentIndex;
                if (startX - endX > getWidth() / 2) {   
                    tempIndex++;
                } else if (endX - startX > getWidth() / 2) {  
                    tempIndex--;
                }
                scrollIndex(tempIndex);
                break;
        }
        return true;
    }

    private class MTouchDetector extends GestureDetector.SimpleOnGestureListener {

        public boolean onDown(MotionEvent e) {
            YYLogUtils.w("MTouchDetector-onDown");
            return super.onDown(e);
        }

        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
          
           //直接移動
           scrollBy((int) distanceX, getScrollY());
           return true;
        }
        ...
    }

      private void scrollIndex(int tempIndex) {
        //第一頁不能滑動
        if (tempIndex < 0) {
            tempIndex = 0;
        }
        //最后一頁不能滑動
        if (tempIndex > getChildCount() - 1) {
            tempIndex = getChildCount() - 1;
        }
        currentIndex = tempIndex;
        mScroller.startScroll(getScrollX(), 0, currentIndex * getWidth() - getScrollX(), 0);
        postInvalidate();
    }
 
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
            postInvalidate();
        }
    }

這樣通過 GestureDetector 結合 Scroller 就可以達到,按著滾動的效果和放開自動滾動到指定索引的效果了。

GestureDetector 確實是很方便,幫助我們封裝了事件的邏輯,我們只需要對相應的時間做出響應即可,我愿稱之為萬能事件處理器。

除了這些單獨的事件的處理,在同一個ViewGroup中如果有多個子View,我們還能通過 ViewDragHelper 來實現(xiàn)子 View 的自由滾動,甚至當其中一個View滾動的同時,我可以做對應的變化,(喲,是不是有behavior那味了)

1.3 子View的滾動與協(xié)調交互

一句話來介紹 ViewDragHelper ,它是用于在 ViewGroup 內部拖動視圖的。

ViewDragHelper 也是谷歌幫我們封裝好的工具類, 其本質就是內部封裝了MotionEvent 和 Scroller,記錄了移動的X和Y,讓 Scroller 去執(zhí)行滾動邏輯,從而實現(xiàn)讓 ViewGroup 內部的子 View 可以實滾動與協(xié)調滾動的邏輯。

如何使用?固定的套路:

    private void initView() {
        //通過回調,告知告訴了移動了多少,觸摸位置,觸摸速度
        viewDragHelper = ViewDragHelper.create(this, callback);
    }
 
    /**
     * 觸摸事件傳遞給ViewDragHelper
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        viewDragHelper.processTouchEvent(event);
        return true;  //傳遞給viewDragHelper。返回true,消費此事件
    }

    /**
     * 是否需要傳遞給viewDragHelper攔截事件
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean result = viewDragHelper.shouldInterceptTouchEvent(ev);
        return result;        //讓傳遞給viewDragHelper判斷是否需要攔截
    }

     //回調處理有很多,根據(jù)不同的需求來實現(xiàn)
     private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {


        @Override     //是否捕獲child的觸摸事件,是否能移動
        public boolean tryCaptureView(View child, int pointerId) {
            return child == redView || child == blueView;  //可以移動紅色view
        }


        @Override  //chlid的移動后的回調,監(jiān)聽
        public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
           // Log.d("tag", "被移動了");
        }


        @Override   //控件水平可拖拽的范圍,目前不能限制邊界,用于手指抬起,view動畫移動到的位置
        public int getViewHorizontalDragRange(View child) {
            return getMeasuredWidth() - child.getMeasuredWidth();
        }


        @Override   //控件垂直可拖拽的范圍,目前不能限制邊界,用于手指抬起,view動畫移動到的位置
        public int getViewVerticalDragRange(View child) {
            return getMeasuredHeight() - child.getMeasuredHeight();
        }


        @Override    //控制水平移動的方向。多少距離,left = child.getleft() + dx;
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            //在這里限制最大的移動距離,不能出邊界
            if (left < 0) {
                left = 0;
            } else if (left > getMeasuredWidth() - child.getMeasuredWidth()) {
                left = getMeasuredWidth() - child.getMeasuredWidth();
            }
            return left;
        }


        @Override      //控制垂直移動的方向。多少距離
        public int clampViewPositionVertical(View child, int top, int dy) {
            //在這里限制最大的移動距離,不能出邊界
            if (top < 0) {
                top = 0;
            } else if (top > getMeasuredHeight() - child.getMeasuredHeight()) {
                top = getMeasuredHeight() - child.getMeasuredHeight();
            }
            return top;
        }


        @Override      //當前child移動后,別的view跟著做對應的移動。用于做伴隨移動
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            //判斷當藍色的移動的時候,紅色跟著移動相同的距離
            if (changedView == blueView) {
                redView.layout(redView.getLeft() + dx, redView.getTop() + dy, redView.getRight()
                        + dx, redView.getBottom() + dy);
            } else if (changedView == redView) {
                blueView.layout(blueView.getLeft() + dx, blueView.getTop() + dy, blueView.getRight()
                        + dx, blueView.getBottom() + dy);
            }
        }


        @Override    //手指抬起后,執(zhí)行相應的邏輯
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            //以分界線判斷在左邊還是右邊
            int centerLeft = getMeasuredWidth() / 2 - releasedChild.getMeasuredWidth() / 2;
            if (releasedChild.getLeft() < centerLeft) {
                //左邊移動。移動到的距離
                viewDragHelper.smoothSlideViewTo(releasedChild, 0, releasedChild.getTop());
                ViewCompat.postInvalidateOnAnimation(DragLayout.this);  //刷新整個view
            } else {
                //右邊移動。移動到的距離
                viewDragHelper.smoothSlideViewTo(releasedChild, getMeasuredWidth() -
                        releasedChild.getMeasuredWidth(), releasedChild.getTop());
                ViewCompat.postInvalidateOnAnimation(DragLayout.this);    //刷新整個view
            }
        }


    };

    @Override
    public void computeScroll() {
        //如果正在移動中,繼續(xù)刷新
        if (viewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(DragLayout.this);
        }
    }

ViewDragHelper (這名字真的取的很好),其實就是滾動(拖拽)的幫助類,可以單獨的滾動 ViewGroup 其中的一個子View,也可以用于多個子View的協(xié)調滾動。

這也是本期側滑菜單選用的方案,多個子View的協(xié)調滾動的應用。

關于更多 ViewDragHelper 的基礎使用,大家如果不了解可以看鴻洋的老文章【傳送門】

關于View/ViewGroup的事件,除了這些常用的之外,還有例如多指觸控事件,縮放的事件 ScaleGestureDecetor 等,由于比較少用,這里就不過多的介紹,其實邏輯與道理都是差不多的,如果有用到的話,可以再查閱對應的文檔哦。

1.4 ViewGroup之間的嵌套與協(xié)調效果

前面講到的都是ViewGroup內部的事件處理,關于ViewGroup之間的嵌套滾動來說的話,其實這是另一個話題了,跟自定義ViewGroup內部的事件處理相比,屬實是另一個分支了,演變?yōu)槎鄠€解決方案,多個知識點了。

我之前的文章有過簡單的介紹,目前主要是分幾種思路

  • NestedScrolling機制
  • CoordinatorLayout + Behavior
  • CoordinatorLayout + AppBarLayout
  • ConstraintLayout / MotionLayout 機制

NestedScrollingParent 與 NestedScrollingChild,NestedScrolling 機制能夠讓父view和子view在滾動時進行配合,其基本流程如下:當子view開始滾動之前,可以通知父view,讓其先于自己進行滾動,子view滾動之后,還可以通知父view繼續(xù)滾動。

可以看看我之前的文章【傳送門】

由于手撕 NestedScrolling 還是有點難度,對于一些嵌套滾動的需求,谷歌推出了 NestedScrollView 來實現(xiàn)嵌套滾動。而對于一些常見的、場景化的協(xié)調效果來說,谷歌推出 CoordinatorLayout 封裝類,可以結合 Behavior 實現(xiàn)一些自定義的協(xié)調效果。

雖說 Behavior 的定義比 NestedScrolling 算簡單一點了,但是也比較復雜啊,有沒有更簡單的,對于一些更常見的場景,谷歌說可以結合 AppBarLayout 做出一些常見的滾動效果。也確實解決了我們大部分滾動效果。

關于這一點可以看看我之前的文章【傳送門】

雖然通過監(jiān)聽 AppBarLayout 的高度變化百分比,可以做出各種各樣的其他布局的協(xié)調動畫效果。但是一個是效率問題,一個是難度問題,總有一些特定的效果無法實現(xiàn)。

所以谷歌推出了 ConstraintLayout / MotionLayout 能更方便的做出各種協(xié)調效果。

關于這一點可以看看我之前的文章【傳送門】

那么到此基本就解決了外部ViewGroup之前的嵌套與協(xié)調問題。

這里就不展開說了,這是另外一個體系,有需求的同學可以自行搜索了解一些。我們還是回歸正題。

關于自定義 ViewGroup 的事件相關,我們就先初步的整理出一個目錄了,接下來我們還是快看看如何定義一個側滑菜單吧。

二、ViewDragHelper的側滑菜單實現(xiàn)

目錄列好了之后,我們就可以按需選擇或組合就可以實現(xiàn)對應的效果。

比如我們這一期的側滑菜單,其實就是涉及到了交互與嵌套的問題,而我們通過上述的學習,我們就知道我們可以有多種方式來實現(xiàn)。

  • 比如手撕 onTouchEvent + Scroller(為了自動返回)
  • 再簡單點 GestureDetector + Scroller(為了自動返回)
  • 再簡單點 ViewDragHelper 即可(就是對Scroller的封裝)

我們這里就以最簡單的 ViewDragHelper 方案來實現(xiàn)

我們分為內容布局和右側隱藏的刪除布局,默認的布局方式是內容布局占滿布局寬度,讓刪除布局到屏幕外。

首先我們要測量與布局:

private View contentView;
private View deleteView;
private int contentWidth;
private int contentHeight;
private int deleteWidth;
private int deleteHeight;

public class SwipeLayout extends FrameLayout {

    //完成初始化,獲取控件
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        contentView = getChildAt(0);
        deleteView = getChildAt(1);
    }
  
    //完成測量,獲取高度,寬度
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        contentWidth = contentView.getMeasuredWidth();
        contentHeight = contentView.getMeasuredHeight();
        deleteWidth = deleteView.getMeasuredWidth();
        deleteHeight = deleteView.getMeasuredHeight();
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        contentView.layout(0, 0, contentWidth, contentHeight);
        deleteView.layout(contentView.getRight(), 0, contentView.getRight() + deleteWidth, deleteHeight);
    }
}

我們直接繼承 FrameLayout 也不用自行測量了,布局的時候我們布局到屏幕外的右側即可。

接下來我們就使用 viewDragHelper 來操作子View了。都是固定的寫法

    private void init() {
        //是否處理觸摸,是否處理攔截
        viewDragHelper = ViewDragHelper.create(this, callback);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return viewDragHelper.shouldInterceptTouchEvent
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                downY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float moveX = event.getX();
                float moveY = event.getY();
                float dx = moveX - downX;
                float dy = moveY - downY;
                if (Math.abs(dx) > Math.abs(dy)) {
                    //在水平移動。請求父類不要攔截
                    requestDisallowInterceptTouchEvent(true);
                }
                downX = moveX;
                downY = moveY;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }

        viewDragHelper.processTouchEvent(event);
        return true;
    }

注意的是這里對攔截的事件做了方向上的判斷,都是已學的內容。接下來的重點就是 callback 回調的處理。

    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {

        //點擊ContentView和右側的DeleteView都可以觸發(fā)事件
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return child == contentView || child == deleteView;
        }

        //控件水平可拖拽的范圍,最多也就拖出一個右側DeleteView的寬度
        @Override
        public int getViewHorizontalDragRange(View child) {
            return deleteWidth;
        }

        //控制水平移動的方向距離
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            //做邊界的限制
            if (child == contentView) {
                if (left > 0) left = 0;
                if (left < -deleteWidth) left = -deleteWidth;
            } else if (child == deleteView) {
                if (left > contentWidth) left = contentWidth;
                if (left < contentWidth - deleteWidth) left = contentWidth - deleteWidth;
            }
            return left;
        }

        //當前child移動后,別的view跟著做對應的移動。用于做伴隨移動
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            //做內容布局移動的時候,刪除布局跟著同樣的移動
            if (changedView == contentView) {
                deleteView.layout(deleteView.getLeft() + dx, deleteView.getTop() + dy,
                        deleteView.getRight() + dx, deleteView.getBottom() + dy);
            } else if (changedView == deleteView) {
                //當刪除布局移動的時候,內容布局做同樣的移動
                contentView.layout(contentView.getLeft() + dx, contentView.getTop() + dy,
                        contentView.getRight() + dx, contentView.getBottom() + dy);
            }

        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            //松開之后,緩慢滑動,看是到打開狀態(tài)還是到關閉狀態(tài)
            if (contentView.getLeft() < -deleteWidth / 2) {
                //打開
                open();
            } else {
                //關閉
                close();
            }
        }
    };
    /**
     * 打開開關的的方法
     */
    public void open() {
        viewDragHelper.smoothSlideViewTo(contentView, -deleteWidth, 0);
        ViewCompat.postInvalidateOnAnimation(SwipeLayout.this);
    }

    /**
     * 關閉開關的方法
     */
    public void close() {
        viewDragHelper.smoothSlideViewTo(contentView, 0, 0);
        ViewCompat.postInvalidateOnAnimation(SwipeLayout.this);
    }

    /**
     * 重寫移動的方法
     */
    @Override
    public void computeScroll() {
        if (viewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(SwipeLayout.this);
        }
    }

已經做了詳細的注釋了,是不是很清楚了呢? 效果圖如下:

三、回調與封裝

在一些列表上使用的時候我們需要一個Item只能打開一個刪除布局,那么我們需要一個管理類來管理,手動的打開和關閉刪除布局。

public class SwipeLayoutManager {

    private SwipeLayoutManager() {
    }

    private static SwipeLayoutManager mInstance = new SwipeLayoutManager();

    public static SwipeLayoutManager getInstance() {
        return mInstance;
    }


    //記錄當前打開的item
    private SwipeLayout currentSwipeLayout;


    public void setSwipeLayout(SwipeLayout layout) {
        this.currentSwipeLayout = layout;
    }


    //關閉當前打開的item。layout
    public void closeCurrentLayout() {
        if (currentSwipeLayout != null) {
            currentSwipeLayout.close();  //調用的自定義控件的close方法
            currentSwipeLayout=null;
        }
    }

    public boolean isShouldSwipe(SwipeLayout layout) {
        if (currentSwipeLayout == null) {
            //沒有打開
            return true;
        } else {
            //有打開的
            return currentSwipeLayout == layout;
        }
    }

    //清空currentLayout
    public void clearCurrentLayout() {
        currentSwipeLayout = null;
    }


}

我們還需要對打開關閉的狀態(tài)做管理

    enum SwipeState {
        Open, Close;
    }

    private SwipeState currentState = SwipeState.Close; //默認為關閉

如果是打開的狀態(tài),我們還需要對事件做攔截的處理

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean result = viewDragHelper.shouldInterceptTouchEvent(ev);
        if (!SwipeLayoutManager.getInstance().isShouldSwipe(this)) {
            //在此關閉已經打開的item。
            SwipeLayoutManager.getInstance().closeCurrentLayout();
            result = true;
        }
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //如果當前的是打開的,下面的邏輯不能執(zhí)行了
        if (!SwipeLayoutManager.getInstance().isShouldSwipe(this)) {
            requestDisallowInterceptTouchEvent(true);
            return true;
        }

        ...
    }

回調的處理,在 onViewPositionChanged 的移動回調中,我們可以通過內容布局的left是否為0 或者 -deleteWidth 就可以判斷當前的布局狀態(tài)是否是打開狀態(tài)。

    private OnSwipeStateChangeListener listener;

    public void seOnSwipeStateChangeListener(OnSwipeStateChangeListener listener) {
        this.listener = listener;
    }

    public interface OnSwipeStateChangeListener {
        void Open();

        void Close();
    }

    ...

       //當前child移動后,別的view跟著做對應的移動。用于做伴隨移動
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            //做內容布局移動的時候,刪除布局跟著同樣的移動
            if (changedView == contentView) {
                deleteView.layout(deleteView.getLeft() + dx, deleteView.getTop() + dy,
                        deleteView.getRight() + dx, deleteView.getBottom() + dy);
            } else if (changedView == deleteView) {
                //當刪除布局移動的時候,內容布局做同樣的移動
                contentView.layout(contentView.getLeft() + dx, contentView.getTop() + dy,
                        contentView.getRight() + dx, contentView.getBottom() + dy);
            }

            //判斷開,關的邏輯
            if (contentView.getLeft() == 0 && currentState != SwipeState.Close) {
                //關閉刪除欄.刪除實例
                currentState = SwipeState.Close;
                if (listener != null) {
                    listener.Close();    //在此回調關閉方法
                }
                SwipeLayoutManager.getInstance().clearCurrentLayout();
            } else if (contentView.getLeft() == -deleteWidth && currentState != SwipeState.Open) {
                //開啟刪除欄。獲取實例
                currentState = SwipeState.Open;
                if (listener != null) {
                    listener.Open();     //在此回調打開方法
                }
                SwipeLayoutManager.getInstance().setSwipeLayout(SwipeLayout.this);
            }
        }

這樣就完成了全部的邏輯啦,其實理解之后并不復雜。

后記

其實關于側滑返回的效果,網絡上有很多的方案,這也只是其中的一種,為了方便大家理解 viewDragHelper 的使用,其實它還可以用于很多其他的場景,比如底部菜單的展示,Grid網格的動態(tài)變換等等。

到此這篇關于Android自定義ViewGroup實現(xiàn)側滑菜單的文章就介紹到這了,更多相關Android ViewGroup側滑菜單內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Intent傳遞對象之Serializable和Parcelable的區(qū)別

    Intent傳遞對象之Serializable和Parcelable的區(qū)別

    Intent在不同的組件中傳遞對象數(shù)據(jù)的應用非常普遍,大家都知道在intent傳遞對象的方法有兩種:1、實現(xiàn)Serializable接口、2、實現(xiàn)Parcelable接口,接下來通過本文給大家介紹Intent傳遞對象之Serializable和Parcelable的區(qū)別,感興趣的朋友一起學習吧
    2016-01-01
  • Android Camera2采集攝像頭原始數(shù)據(jù)

    Android Camera2采集攝像頭原始數(shù)據(jù)

    這篇文章主要介紹了Android Camera2采集攝像頭原始數(shù)據(jù)并進行手工預覽的功能實現(xiàn)原理以及代碼分析,需要的朋友學習下吧。
    2018-02-02
  • Android編程之圖片相關代碼集錦

    Android編程之圖片相關代碼集錦

    這篇文章主要介紹了Android編程之圖片相關代碼集錦,實例總結了大量Android圖片操作相關代碼,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-11-11
  • 詳解Android?GLide圖片加載常用幾種方法

    詳解Android?GLide圖片加載常用幾種方法

    這篇文章主要為大家介紹了詳解Android?GLide圖片加載常用幾種方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-11-11
  • Android intent之間復雜參數(shù)傳遞方法詳解

    Android intent之間復雜參數(shù)傳遞方法詳解

    這篇文章主要介紹了Android intent之間復雜參數(shù)傳遞方法,較為詳細的分析了Android中intent參數(shù)傳遞的常見方法與使用技巧,需要的朋友可以參考下
    2016-10-10
  • Android TextView使用SpannableString設置復合文本的方法詳解

    Android TextView使用SpannableString設置復合文本的方法詳解

    這篇文章主要介紹了Android TextView使用SpannableString設置復合文本的方法,結合實例形式詳細分析了Android中SpannableString類的功能及相關用法,需要的朋友可以參考下
    2016-08-08
  • Android中HTTP請求中文亂碼解決辦法

    Android中HTTP請求中文亂碼解決辦法

    這篇文章主要介紹了Android中HTTP請求中文亂碼解決辦法的相關資料,希望通過本文能幫助到大家,讓大家解決中文亂碼的問題,需要的朋友可以參考下
    2017-09-09
  • Service Activity的三種交互方式(詳解)

    Service Activity的三種交互方式(詳解)

    下面小編就為大家?guī)硪黄猄ervice Activity的三種交互方式(詳解)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-09-09
  • Android圖像處理之繪制圓形、三角形及扇形的頭像

    Android圖像處理之繪制圓形、三角形及扇形的頭像

    這篇文章主要給大家介紹了Android圖像處理之繪制圓形、三角形及扇形頭像的相關資料,文中給出了詳細的代碼示例,通過學會了文中的方法,就不局限于圓形頭像了,剛興趣的朋友們下面跟著小編一起來學習學習吧。
    2017-04-04
  • Android不壓縮圖片實現(xiàn)高清加載巨圖實例

    Android不壓縮圖片實現(xiàn)高清加載巨圖實例

    這篇文章主要為大家介紹了Android不壓縮圖片實現(xiàn)高清加載巨圖實例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-06-06

最新評論