自定义View(7) -- 酷狗侧滑菜单

效果图

上一篇我们自定义了一个流式布局的ViewGroup,我们为了熟悉自定义ViewGroup,就继续自定义ViewGroup。这篇的内容是是仿照酷狗的侧滑菜单。
我们写代码之前,先想清楚是怎么实现,解析实现的步骤。实现侧滑的方式很多种,在这里我选择继承HorizontalScrollView,为什么继承这个呢?因为继承这个的话,我们就不用写childViewmove meause layout,这样就节约了很大的代码量和事件,因为内部HorizontalScrollView已经封装好了。我们在这个控件里面放置两个childView,一个是menu,一个是content。然后我们处理拦截和快速滑动事件就可以了。思路想清楚了我们就开始撸码。
首先我们自定义一个属性,用于打开的时候content还有多少可以看到,也就是打开的时候menu距离右边的距离。

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

在初始化的时候我们通过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));
        // 菜单页的宽度是 = 屏幕的宽度 - 右边的一小部分距离(自定义属性)
        mMenuWidth = (int) (DisplayUtil.getScreenWidth(context) - rightMargin);
        array.recycle();
    }

接着我们在布局加载完毕的时候我们指定menucontent的宽度

//xml 布局解析完毕回调的方法
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        //指定宽高
        //先拿到整体容器
        ViewGroup container = (ViewGroup) getChildAt(0);

        int childCount = container.getChildCount();
        if (childCount != 2)
            throw new RuntimeException("只能放置两个子View");
        //菜单
        mMenuView = container.getChildAt(0);
        ViewGroup.LayoutParams meauParams = mMenuView.getLayoutParams();
        meauParams.width = mMenuWidth;
        //7.0一下的不加这句代码是正常的   7.0以上的必须加
        mMenuView.setLayoutParams(meauParams);

        //内容页
        mContentView = container.getChildAt(1);
        ViewGroup.LayoutParams contentParams = mContentView.getLayoutParams();
        contentParams.width = DisplayUtil.getScreenWidth(getContext());
        //7.0一下的不加这句代码是正常的   7.0以上的必须加
        mContentView.setLayoutParams(contentParams);
    }

这里有一个细节,我们在刚进入的时候,菜单默认是关闭的,所以我们需要调用scrollTo()函数移动一下位置,但是发现在onFinishInflate()函数里面调用没有作用,这个是为什么呢?因为我们在xml加载完毕之后,才会真正的执行View的绘制流程,这时候调用scrollTo()这个函数其实是执行了代码的,但是在onLaout()摆放childView的时候,又默认回到了(0,0)位置,所以我们应该在onLayout()之后调用这个函数

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        //进入是关闭状态
        scrollTo(mMenuWidth, 0);
    }

初始化完毕了,接下来我们进行事件的拦截,MOVE的时候相应滑动事件,UP的时候判断是关闭还是打开,然后调用函数即可


//手指抬起是二选一,要么关闭要么打开
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        //    当菜单打开的时候,手指触摸右边内容部分需要关闭菜单,还需要拦截事件(打开情况下点击内容页不会响应点击事件)
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            // 只需要管手指抬起 ,根据我们当前滚动的距离来判断
            int currentScrollX = getScrollX();
            if (currentScrollX > mMenuWidth / 2) {
                // 关闭
                closeMenu();
            } else {
                // 打开
                openMenu();
            }
            return true;
        }
        return super.onTouchEvent(ev);
    }

    /**
     * 打开菜单 滚动到 0 的位置
     */
    private void openMenu() {
        // smoothScrollTo 有动画
        smoothScrollTo(0, 0);
    }

    /**
     * 关闭菜单 滚动到 mMenuWidth 的位置
     */
    private void closeMenu() {
        smoothScrollTo(mMenuWidth, 0);
    }

到这的话,滑动事件和打开关闭事件都完成了,接下来我们就处理一个效果的问题,这里当从左往右滑动的时候,是慢慢打开菜单,这时候content是有一个慢慢的缩放,menu有一个放大和透明度变小,而反过来关闭菜单的话就是相反的效果,content慢慢放大,menu缩小和透明度变大。这里还有一个细节,就是menu慢慢的退出和进入,滑动的距离不是和移动的距离相同的,所以这里还有一个平移。接下来重写onScrollChanged()函数,然后计算出一个梯度值来做处理

 //滑动改变触发
    @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  手指从左往右滑动 由大变小
        //计算一个梯度值 1->0
        float scale = 1.0f * l / mMenuWidth;

        //酷狗侧滑效果...
//        //右边的缩放 最小是0.7f ,最大是1.0f
        float rightScale = 0.7f + 0.3f * scale;
        //设置mContentView缩放的中心点位置
        ViewCompat.setPivotX(mContentView, 0);
        ViewCompat.setPivotY(mContentView, mContentView.getHeight() / 2);
        //设置右边缩放
        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);
    }

这样的话我们就完成了效果,但是我们还有几个细节没有处理,首先是快速滑动的问题,还有一个是当打开menu的时候,点击content需要关闭菜单,而不是相应对应的事件。接下来我们对这两个问题进行处理。

快速滑动问题,这个问题我们采用GestureDetector这个类来做处理,这个类可以处理很多收拾问题:


/**
     * 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);
    }

这里我们主要是响应onFling()这个函数,然后判断当前是打开还是关闭状态,在根据快速滑动的手势来执行打开还是关闭的操作:

 @Override
    public boolean onTouchEvent(MotionEvent ev) {
          if (mGestureDetector.onTouchEvent(ev))//快速滑动触发了下面的就不要执行了
            return true;      
      
            //....
    }


//快速滑动
    private GestureDetector.OnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            //快速滑动回调
            //打开的时候从右到左滑动关闭   关闭的时候从左往右打开
//            Log.e("zzz", "velocityX->" + velocityX);
            // >0 从左往右边滑动  <0 从右到左
            if (mMenuIsOpen) {
                if (velocityX < 0) {
                    closeMenu();
                    return true;
                }
            } else {
                if (velocityX > 0) {
                    openMenu();
                    return true;
                }
            }
            return super.onFling(e1, e2, velocityX, velocityY);
        }
    };

接下来处理menu打开状态下点击content关闭menu,这里我们需要用到onInterceptTouchEvent。当打开状态的时候,我们就把这个事件拦截,然后关闭菜单即可。但是这里有一个问题,当我们拦截了DOWN事件之后,后面的MOVE UP事件都会被拦截并且相应自身的onTouchEvent事件,所以这里我们需要添加一个判断值,判断是否拦截,然后让其onTouchEvent是否继续执行操作

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        isIntercept = false;
        if (mMenuIsOpen && ev.getX() > mMenuWidth) {//打开状态  触摸右边关闭
            isIntercept = true;//拦截的话就不执行自己的onTouchEvent
            closeMenu();
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

 @Override
    public boolean onTouchEvent(MotionEvent ev) {

        if (isIntercept)//拦截的话就不执行自己的onTouchEvent
            return true;
    //...
}

根据我们提出需求,然后分析需求,再完成需求。这一步步我们慢慢进行渗透,最终完成效果,完成之后你会发现其实也就那么一回事。当我们有新需求的时候,我们应该不要恐惧,应该欣然乐观的接收,再慢慢分析,最终完成。这样的话我们才能提高我们的技术。

本文源码下载地址:https://github.com/ChinaZeng/CustomView

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,980评论 25 707
  • Android自定义控件并没有什么捷径可走,需要不断得模仿练习才能出师。这其中进行模仿练习的demo的选择是至关重...
    cv大法师阅读 964评论 16 32
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,383评论 0 17
  • 寒风起,秋叶落,雨打斜阳,原本萧瑟无比的街道,此刻更显冷清,使人好不生气! 月雅清在这个死气沉沉的...
    木又欠阅读 238评论 0 0
  • 从前的想法总是,越多越好 无论是情感还是物品 所以,多多少少称得上收集癖 可是,不料等到物品越来越多的时候 家里却...
    MacaroonBB阅读 264评论 0 0