自定義View(7) -- 酷狗側(cè)滑菜單

效果圖

上一篇我們自定義了一個(gè)流式布局的ViewGroup,我們?yōu)榱耸煜ぷ远xViewGroup,就繼續(xù)自定義ViewGroup。這篇的內(nèi)容是是仿照酷狗的側(cè)滑菜單。
我們寫(xiě)代碼之前,先想清楚是怎么實(shí)現(xiàn),解析實(shí)現(xiàn)的步驟。實(shí)現(xiàn)側(cè)滑的方式很多種,在這里我選擇繼承HorizontalScrollView,為什么繼承這個(gè)呢?因?yàn)槔^承這個(gè)的話,我們就不用寫(xiě)childViewmove meause layout,這樣就節(jié)約了很大的代碼量和事件,因?yàn)閮?nèi)部HorizontalScrollView已經(jīng)封裝好了。我們?cè)谶@個(gè)控件里面放置兩個(gè)childView,一個(gè)是menu,一個(gè)是content。然后我們處理攔截和快速滑動(dòng)事件就可以了。思路想清楚了我們就開(kāi)始擼碼。
首先我們自定義一個(gè)屬性,用于打開(kāi)的時(shí)候content還有多少可以看到,也就是打開(kāi)的時(shí)候menu距離右邊的距離。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SkiddingMenuLayout">
        <attr name="menuRightMargin" format="dimension"/>
    </declare-styleable>
</resources>

在初始化的時(shí)候我們通過(guò)menuRightMargin屬性獲取menu真正的寬度

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

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

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


        // 初始化自定義屬性
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SkiddingMenuLayout);

        float rightMargin = array.getDimension(
                R.styleable.SkiddingMenuLayout_menuRightMargin, DisplayUtil.dip2px(context, 50));
        // 菜單頁(yè)的寬度是 = 屏幕的寬度 - 右邊的一小部分距離(自定義屬性)
        mMenuWidth = (int) (DisplayUtil.getScreenWidth(context) - rightMargin);
        array.recycle();
    }

接著我們?cè)诓季旨虞d完畢的時(shí)候我們指定menucontent的寬度

//xml 布局解析完畢回調(diào)的方法
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        //指定寬高
        //先拿到整體容器
        ViewGroup container = (ViewGroup) getChildAt(0);

        int childCount = container.getChildCount();
        if (childCount != 2)
            throw new RuntimeException("只能放置兩個(gè)子View");
        //菜單
        mMenuView = container.getChildAt(0);
        ViewGroup.LayoutParams meauParams = mMenuView.getLayoutParams();
        meauParams.width = mMenuWidth;
        //7.0一下的不加這句代碼是正常的   7.0以上的必須加
        mMenuView.setLayoutParams(meauParams);

        //內(nèi)容頁(yè)
        mContentView = container.getChildAt(1);
        ViewGroup.LayoutParams contentParams = mContentView.getLayoutParams();
        contentParams.width = DisplayUtil.getScreenWidth(getContext());
        //7.0一下的不加這句代碼是正常的   7.0以上的必須加
        mContentView.setLayoutParams(contentParams);
    }

這里有一個(gè)細(xì)節(jié),我們?cè)趧傔M(jìn)入的時(shí)候,菜單默認(rèn)是關(guān)閉的,所以我們需要調(diào)用scrollTo()函數(shù)移動(dòng)一下位置,但是發(fā)現(xiàn)在onFinishInflate()函數(shù)里面調(diào)用沒(méi)有作用,這個(gè)是為什么呢?因?yàn)槲覀冊(cè)?code>xml加載完畢之后,才會(huì)真正的執(zhí)行View的繪制流程,這時(shí)候調(diào)用scrollTo()這個(gè)函數(shù)其實(shí)是執(zhí)行了代碼的,但是在onLaout()擺放childView的時(shí)候,又默認(rèn)回到了(0,0)位置,所以我們應(yīng)該在onLayout()之后調(diào)用這個(gè)函數(shù)

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        //進(jìn)入是關(guān)閉狀態(tài)
        scrollTo(mMenuWidth, 0);
    }

初始化完畢了,接下來(lái)我們進(jìn)行事件的攔截,MOVE的時(shí)候相應(yīng)滑動(dòng)事件,UP的時(shí)候判斷是關(guān)閉還是打開(kāi),然后調(diào)用函數(shù)即可


//手指抬起是二選一,要么關(guān)閉要么打開(kāi)
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        //    當(dāng)菜單打開(kāi)的時(shí)候,手指觸摸右邊內(nèi)容部分需要關(guān)閉菜單,還需要攔截事件(打開(kāi)情況下點(diǎn)擊內(nèi)容頁(yè)不會(huì)響應(yīng)點(diǎn)擊事件)
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            // 只需要管手指抬起 ,根據(jù)我們當(dāng)前滾動(dòng)的距離來(lái)判斷
            int currentScrollX = getScrollX();
            if (currentScrollX > mMenuWidth / 2) {
                // 關(guān)閉
                closeMenu();
            } else {
                // 打開(kāi)
                openMenu();
            }
            return true;
        }
        return super.onTouchEvent(ev);
    }

    /**
     * 打開(kāi)菜單 滾動(dòng)到 0 的位置
     */
    private void openMenu() {
        // smoothScrollTo 有動(dòng)畫(huà)
        smoothScrollTo(0, 0);
    }

    /**
     * 關(guān)閉菜單 滾動(dòng)到 mMenuWidth 的位置
     */
    private void closeMenu() {
        smoothScrollTo(mMenuWidth, 0);
    }

到這的話,滑動(dòng)事件和打開(kāi)關(guān)閉事件都完成了,接下來(lái)我們就處理一個(gè)效果的問(wèn)題,這里當(dāng)從左往右滑動(dòng)的時(shí)候,是慢慢打開(kāi)菜單,這時(shí)候content是有一個(gè)慢慢的縮放,menu有一個(gè)放大和透明度變小,而反過(guò)來(lái)關(guān)閉菜單的話就是相反的效果,content慢慢放大,menu縮小和透明度變大。這里還有一個(gè)細(xì)節(jié),就是menu慢慢的退出和進(jìn)入,滑動(dòng)的距離不是和移動(dòng)的距離相同的,所以這里還有一個(gè)平移。接下來(lái)重寫(xiě)onScrollChanged()函數(shù),然后計(jì)算出一個(gè)梯度值來(lái)做處理

 //滑動(dòng)改變觸發(fā)
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);

//        //抽屜效果  兩種一樣
//        ViewCompat.setTranslationX(mMenuView, l);
//        ViewCompat.setX(mMenuView, l);

//        Log.e("zzz", "l->" + l + " t->" + t + " oldl->" + oldl + " oldt->" + oldt);
        //主要看l  手指從左往右滑動(dòng) 由大變小
        //計(jì)算一個(gè)梯度值 1->0
        float scale = 1.0f * l / mMenuWidth;

        //酷狗側(cè)滑效果...
//        //右邊的縮放 最小是0.7f ,最大是1.0f
        float rightScale = 0.7f + 0.3f * scale;
        //設(shè)置mContentView縮放的中心點(diǎn)位置
        ViewCompat.setPivotX(mContentView, 0);
        ViewCompat.setPivotY(mContentView, mContentView.getHeight() / 2);
        //設(shè)置右邊縮放
        ViewCompat.setScaleX(mContentView, rightScale);
        ViewCompat.setScaleY(mContentView, rightScale);

        //菜單
        //透明度是半透明到全透明  0.5f-1.0f
        float alpha = 0.5f + (1.0f - scale) * 0.5f;
        ViewCompat.setAlpha(mMenuView, alpha);

        //縮放  0.7-1.0
        float leftScale = 0.7f + 0.3f * (1 - scale);
        ViewCompat.setScaleX(mMenuView, leftScale);
        ViewCompat.setScaleY(mMenuView, leftScale);

        //退出按鈕在右邊
        ViewCompat.setTranslationX(mMenuView, 0.2f * l);
    }

這樣的話我們就完成了效果,但是我們還有幾個(gè)細(xì)節(jié)沒(méi)有處理,首先是快速滑動(dòng)的問(wèn)題,還有一個(gè)是當(dāng)打開(kāi)menu的時(shí)候,點(diǎn)擊content需要關(guān)閉菜單,而不是相應(yīng)對(duì)應(yīng)的事件。接下來(lái)我們對(duì)這兩個(gè)問(wèn)題進(jìn)行處理。

快速滑動(dòng)問(wèn)題,這個(gè)問(wèn)題我們采用GestureDetector這個(gè)類來(lái)做處理,這個(gè)類可以處理很多收拾問(wèn)題:


/**
     * The listener that is used to notify when gestures occur.
     * If you want to listen for all the different gestures then implement
     * this interface. If you only want to listen for a subset it might
     * be easier to extend {@link SimpleOnGestureListener}.
     */
    public interface OnGestureListener {

        /**
         * Notified when a tap occurs with the down {@link MotionEvent}
         * that triggered it. This will be triggered immediately for
         * every down event. All other events should be preceded by this.
         *
         * @param e The down motion event.
         */
        boolean onDown(MotionEvent e);

        /**
         * The user has performed a down {@link MotionEvent} and not performed
         * a move or up yet. This event is commonly used to provide visual
         * feedback to the user to let them know that their action has been
         * recognized i.e. highlight an element.
         *
         * @param e The down motion event
         */
        void onShowPress(MotionEvent e);

        /**
         * Notified when a tap occurs with the up {@link MotionEvent}
         * that triggered it.
         *
         * @param e The up motion event that completed the first tap
         * @return true if the event is consumed, else false
         */
        boolean onSingleTapUp(MotionEvent e);

        /**
         * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
         * current move {@link MotionEvent}. The distance in x and y is also supplied for
         * convenience.
         *
         * @param e1 The first down motion event that started the scrolling.
         * @param e2 The move motion event that triggered the current onScroll.
         * @param distanceX The distance along the X axis that has been scrolled since the last
         *              call to onScroll. This is NOT the distance between {@code e1}
         *              and {@code e2}.
         * @param distanceY The distance along the Y axis that has been scrolled since the last
         *              call to onScroll. This is NOT the distance between {@code e1}
         *              and {@code e2}.
         * @return true if the event is consumed, else false
         */
        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);

        /**
         * Notified when a long press occurs with the initial on down {@link MotionEvent}
         * that trigged it.
         *
         * @param e The initial on down motion event that started the longpress.
         */
        void onLongPress(MotionEvent e);

        /**
         * Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
         * and the matching up {@link MotionEvent}. The calculated velocity is supplied along
         * the x and y axis in pixels per second.
         *
         * @param e1 The first down motion event that started the fling.
         * @param e2 The move motion event that triggered the current onFling.
         * @param velocityX The velocity of this fling measured in pixels per second
         *              along the x axis.
         * @param velocityY The velocity of this fling measured in pixels per second
         *              along the y axis.
         * @return true if the event is consumed, else false
         */
        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
    }

    /**
     * The listener that is used to notify when a double-tap or a confirmed
     * single-tap occur.
     */
    public interface OnDoubleTapListener {
        /**
         * Notified when a single-tap occurs.
         * <p>
         * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
         * will only be called after the detector is confident that the user's
         * first tap is not followed by a second tap leading to a double-tap
         * gesture.
         *
         * @param e The down motion event of the single-tap.
         * @return true if the event is consumed, else false
         */
        boolean onSingleTapConfirmed(MotionEvent e);
 
        /**
         * Notified when a double-tap occurs.
         *
         * @param e The down motion event of the first tap of the double-tap.
         * @return true if the event is consumed, else false
         */
        boolean onDoubleTap(MotionEvent e);

        /**
         * Notified when an event within a double-tap gesture occurs, including
         * the down, move, and up events.
         *
         * @param e The motion event that occurred during the double-tap gesture.
         * @return true if the event is consumed, else false
         */
        boolean onDoubleTapEvent(MotionEvent e);
    }

 /**
     * The listener that is used to notify when a context click occurs. When listening for a
     * context click ensure that you call {@link #onGenericMotionEvent(MotionEvent)} in
     * {@link View#onGenericMotionEvent(MotionEvent)}.
     */
    public interface OnContextClickListener {
        /**
         * Notified when a context click occurs.
         *
         * @param e The motion event that occurred during the context click.
         * @return true if the event is consumed, else false
         */
        boolean onContextClick(MotionEvent e);
    }

這里我們主要是響應(yīng)onFling()這個(gè)函數(shù),然后判斷當(dāng)前是打開(kāi)還是關(guān)閉狀態(tài),在根據(jù)快速滑動(dòng)的手勢(shì)來(lái)執(zhí)行打開(kāi)還是關(guān)閉的操作:

 @Override
    public boolean onTouchEvent(MotionEvent ev) {
          if (mGestureDetector.onTouchEvent(ev))//快速滑動(dòng)觸發(fā)了下面的就不要執(zhí)行了
            return true;      
      
            //....
    }


//快速滑動(dòng)
    private GestureDetector.OnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            //快速滑動(dòng)回調(diào)
            //打開(kāi)的時(shí)候從右到左滑動(dòng)關(guān)閉   關(guān)閉的時(shí)候從左往右打開(kāi)
//            Log.e("zzz", "velocityX->" + velocityX);
            // >0 從左往右邊滑動(dòng)  <0 從右到左
            if (mMenuIsOpen) {
                if (velocityX < 0) {
                    closeMenu();
                    return true;
                }
            } else {
                if (velocityX > 0) {
                    openMenu();
                    return true;
                }
            }
            return super.onFling(e1, e2, velocityX, velocityY);
        }
    };

接下來(lái)處理menu打開(kāi)狀態(tài)下點(diǎn)擊content關(guān)閉menu,這里我們需要用到onInterceptTouchEvent。當(dāng)打開(kāi)狀態(tài)的時(shí)候,我們就把這個(gè)事件攔截,然后關(guān)閉菜單即可。但是這里有一個(gè)問(wèn)題,當(dāng)我們攔截了DOWN事件之后,后面的MOVE UP事件都會(huì)被攔截并且相應(yīng)自身的onTouchEvent事件,所以這里我們需要添加一個(gè)判斷值,判斷是否攔截,然后讓其onTouchEvent是否繼續(xù)執(zhí)行操作

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        isIntercept = false;
        if (mMenuIsOpen && ev.getX() > mMenuWidth) {//打開(kāi)狀態(tài)  觸摸右邊關(guān)閉
            isIntercept = true;//攔截的話就不執(zhí)行自己的onTouchEvent
            closeMenu();
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

 @Override
    public boolean onTouchEvent(MotionEvent ev) {

        if (isIntercept)//攔截的話就不執(zhí)行自己的onTouchEvent
            return true;
    //...
}

根據(jù)我們提出需求,然后分析需求,再完成需求。這一步步我們慢慢進(jìn)行滲透,最終完成效果,完成之后你會(huì)發(fā)現(xiàn)其實(shí)也就那么一回事。當(dāng)我們有新需求的時(shí)候,我們應(yīng)該不要恐懼,應(yīng)該欣然樂(lè)觀的接收,再慢慢分析,最終完成。這樣的話我們才能提高我們的技術(shù)。

本文源碼下載地址:https://github.com/ChinaZeng/CustomView

最后編輯于
?著作權(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)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,209評(píng)論 25 708
  • Android自定義控件并沒(méi)有什么捷徑可走,需要不斷得模仿練習(xí)才能出師。這其中進(jìn)行模仿練習(xí)的demo的選擇是至關(guān)重...
    cv大法師閱讀 1,062評(píng)論 16 32
  • ¥開(kāi)啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開(kāi)一個(gè)線程,因...
    小菜c閱讀 7,362評(píng)論 0 17
  • 寒風(fēng)起,秋葉落,雨打斜陽(yáng),原本蕭瑟無(wú)比的街道,此刻更顯冷清,使人好不生氣! 月雅清在這個(gè)死氣沉沉的...
    木又欠閱讀 256評(píng)論 0 0
  • 從前的想法總是,越多越好 無(wú)論是情感還是物品 所以,多多少少稱得上收集癖 可是,不料等到物品越來(lái)越多的時(shí)候 家里卻...
    MacaroonBB閱讀 285評(píng)論 0 0

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