Android之SlidingMenu源码分析

本文详细分析了Android库SlidingMenu的原理和使用,包括SlidingMenu的类结构,如何创建对象并设置参数,如何将Activity视图与菜单栏结合,以及滑动显示左右菜单栏的源码细节,特别关注了事件分发和拦截,以及CustomViewBehind中菜单栏的滚动显示/隐藏机制。通过对SlidingMenu的源码分析,帮助开发者更好地理解和应用这一组件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

 

1.SlidingMenu是什么

1.1SlidingMenu类结构

2.SlidingMenu使用及源码分析

2.1新建SlidingMenu对象

2.2slidingMenu基础参数设置

2.3将Activity视图添加到SlidingMenu下的上层视图

2.3设置SlidingView菜单栏视图

2.4显示SlidingView内容

3.SlidingMenu下实现滑动显示左右菜单栏源码分析

3.1按下事件监听

3.2滑动事件监听

3.3手势抬起

3.4事件的分发和拦截

3.5当前页面点击返回按钮时关闭菜单栏

4.CustomViewBehind

4.1菜单栏的滚动显示/隐藏

4.1.1只分析LEFT的情况(请他情况同理)

4.1.2在CustomViewAbove重写dispatchDraw方法,调用CustomViewAbove绘制阴影,淡入和选择器方法实现绘制


1.SlidingMenu是什么

SlidingMenu是一个开源的Android库,支持左右菜单栏,它允许开发者使用滑动菜单轻松创建应用程序,就像谷歌+、YouTube和Facebook应用程序中流行的那些滑动菜单一样。你可以在你的Android应用程序中自由使用它;

1.1SlidingMenu类结构

SlidingMenu库设计思路三个主要的ViewGroup(SlidingMenu,CustomViewAbove,CustomViewBehind), SlidingMenu包含两个盖在一起的ViewGroup(CustomViewAbove,CustomViewBehind),CustomViewAbove做为上层视图(主要将Activity中setContentView设置的视图添加到上层视图CustomViewAbove),CustomViewBehind做为下层视图(主要将左右菜单栏视图加添加到下层视图);
在CustomViewAbove上滑动时显示下层菜单视图CustomViewBehind;
主ViewGroup(SlidingMenu)作为一个自定义控件,里面的内容和菜单利用自定义属性设置;

public class SlidingMenu extends RelativeLayout {
    private CustomViewAbove mViewAbove;
    private CustomViewBehind mViewBehind;
}

SlidingMenu(继承RelativeLayout),就是这个主ViewGroup,其中又包含两个Activity内容CustomViewAbove和菜单CustomViewBehind(都继承ViewGroup),SlidingMenu提供两个自定义属性,供使用者传入两个布局id,对应主体内容和菜单布局,此外还有其他一些属性供开发者设置菜单效果;

public class SlidingMenu extends RelativeLayout {
    public void setContent(int res) {
        this.setContent(LayoutInflater.from(this.getContext()).inflate(res, (ViewGroup)null));
    }
    //设置Activity主视图
    public void setContent(View view) {
        this.mViewAbove.setContent(view);
        this.showContent();
    }
    
    public void setMenu(int res) {
        this.setMenu(LayoutInflater.from(this.getContext()).inflate(res, (ViewGroup)null));
    }
    //设置左侧菜单栏视图
    public void setMenu(View v) {
        this.mViewBehind.setContent(v);
    }

    public void setSecondaryMenu(int res) {
        this.setSecondaryMenu(LayoutInflater.from(this.getContext()).inflate(res, (ViewGroup)null));
    }
    
    //设置右侧菜单栏视图
    public void setSecondaryMenu(View v) {
        this.mViewBehind.setSecondaryContent(v);
    }
}

2.SlidingMenu使用及源码分析

SlidingMenu类提供API接口调用,同时提供相应属性参数设置;

2.1新建SlidingMenu对象

slidingMenu = new SlidingMenu(this);
public SlidingMenu(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        this.mActionbarOverlay = false;
        LayoutParams behindParams = new LayoutParams(-1, -1);
        //底层视图主要添加左右菜单栏使用
        this.mViewBehind = new CustomViewBehind(context);
        this.addView(this.mViewBehind, behindParams);
        LayoutParams aboveParams = new LayoutParams(-1, -1);
        //上层视图主要添加主视图(当前Activity的视图)
        this.mViewAbove = new CustomViewAbove(context);
        this.addView(this.mViewAbove, aboveParams);
        this.mViewAbove.setCustomViewBehind(this.mViewBehind);
        this.mViewBehind.setCustomViewAbove(this.mViewAbove);
        //设置监听菜单栏打开或者关闭接口
        this.mViewAbove.setOnPageChangeListener(new OnPageChangeListener() {
            public static final int POSITION_OPEN = 0;
            public static final int POSITION_CLOSE = 1;
            public static final int POSITION_SECONDARY_OPEN = 2;

            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            }

            public void onPageSelected(int position) {
                if(position == 0 && SlidingMenu.this.mOpenListener != null) {
                    SlidingMenu.this.mOpenListener.onOpen();
                } else if(position == 1 && SlidingMenu.this.mCloseListener != null) {
                    SlidingMenu.this.mCloseListener.onClose();
                } else if(position == 2 && SlidingMenu.this.mSecondaryOpenListner != null) {
                    SlidingMenu.this.mSecondaryOpenListner.onOpen();
                }

            }
        });
        //设置菜单栏模式,左侧,右侧,左右同时同存在
        //public static final int LEFT = 0;
        //public static final int RIGHT = 1;
        //public static final int LEFT_RIGHT = 2;
        TypedArray ta = context.obtainStyledAttributes(attrs, styleable.SlidingMenu);
        int mode = ta.getInt(styleable.SlidingMenu_mode, 0);
        this.setMode(mode);
        int viewAbove = ta.getResourceId(styleable.SlidingMenu_viewAbove, -1);
        //设置空的主视图
        if(viewAbove != -1) {
            this.setContent(viewAbove);
        } else {
            this.setContent(new FrameLayout(context));
        }
        //设置空的菜单栏视图
        int viewBehind = ta.getResourceId(styleable.SlidingMenu_viewBehind, -1);
        if(viewBehind != -1) {
            this.setMenu(viewBehind);
        } else {
            this.setMenu(new FrameLayout(context));
        }
        //设置显示菜单栏显示操作模式
        //public static final int TOUCHMODE_MARGIN = 0;
        // public static final int TOUCHMODE_FULLSCREEN = 1;
        //public static final int TOUCHMODE_NONE = 2;
        int touchModeAbove = ta.getInt(styleable.SlidingMenu_touchModeAbove, 0);
        this.setTouchModeAbove(touchModeAbove);
        int touchModeBehind = ta.getInt(styleable.SlidingMenu_touchModeBehind, 0);
        this.setTouchModeBehind(touchModeBehind);
        //设置视图显示样式
        int offsetBehind = (int)ta.getDimension(styleable.SlidingMenu_behindOffset, -1.0F);
        int widthBehind = (int)ta.getDimension(styleable.SlidingMenu_behindWidth, -1.0F);
        if(offsetBehind != -1 && widthBehind != -1) {
            throw new IllegalStateException("Cannot set both behindOffset and behindWidth for a SlidingMenu");
        } else {
            if(offsetBehind != -1) {
                this.setBehindOffset(offsetBehind);
            } else if(widthBehind != -1) {
                this.setBehindWidth(widthBehind);
            } else {
                this.setBehindOffset(0);
            }

            float scrollOffsetBehind = ta.getFloat(styleable.SlidingMenu_behindScrollScale, 0.33F);
            this.setBehindScrollScale(scrollOffsetBehind);
            int shadowRes = ta.getResourceId(styleable.SlidingMenu_shadowDrawable, -1);
            if(shadowRes != -1) {
                this.setShadowDrawable(shadowRes);
            }
            //设置阴影效果
            int shadowWidth = (int)ta.getDimension(styleable.SlidingMenu_shadowWidth, 0.0F);
            this.setShadowWidth(shadowWidth);
            boolean fadeEnabled = ta.getBoolean(styleable.SlidingMenu_fadeEnabled, true);
            this.setFadeEnabled(fadeEnabled);
            float fadeDeg = ta.getFloat(styleable.SlidingMenu_fadeDegree, 0.33F);
            this.setFadeDegree(fadeDeg);
            boolean selectorEnabled = ta.getBoolean(styleable.SlidingMenu_selectorEnabled, false);
            this.setSelectorEnabled(selectorEnabled);
            int selectorRes = ta.getResourceId(styleable.SlidingMenu_selectorDrawable, -1);
            if(selectorRes != -1) {
                this.setSelectorDrawable(selectorRes);
            }

            ta.recycle();
        }
    }

2.2slidingMenu基础参数设置

slidingMenu.setMode(SlidingMenu.LEFT_RIGHT);
        // 设置触摸屏幕的模式
        slidingMenu.setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN);
        // 设置渐入渐出效果的值
        slidingMenu.setFadeDegree(0.35f);
        slidingMenu.setFadeEnabled(true);

a.设置菜单栏模式

slidingMenu.setMode(SlidingMenu.LEFT_RIGHT);

/** Constant value for use with setMode(). Puts the menu to the left of the content.
	 */
	 /**方法setMode()使用的常量.把菜单放在内容的左边*/
	public static final int LEFT = 0;
	/** Constant value for use with setMode(). Puts the menu to the right of the content.
	 */
	  /**方法setMode()使用的常量.把菜单放在内容的右边*/
	public static final int RIGHT = 1;
	/** Constant value for use with setMode(). Puts menus to the left and right of the content.
	 */
	  /**方法setMode()使用的常量.把菜单放在内容的左边和右边*/

b.设置触摸屏幕模式
slidingMenu.setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN);

/** Constant value for use with setTouchModeAbove(). Allows the SlidingMenu to be opened with a swipe
	 * gesture on the screen's margin
	 */
	 /**使用setTouchModeAbove()的常量.允许SlidingMenu在屏幕的边缘通过一个猛击的手势打开.
	 */
	public static final int TOUCHMODE_MARGIN = 0;
 
	/** Constant value for use with setTouchModeAbove(). Allows the SlidingMenu to be opened with a swipe
	 * gesture anywhere on the screen
	 */
	 /**使用setTouchModeAbove()的常量.允许SlidingMenu在屏幕的任何地方通过一个猛击的手势打开.
	 */
	public static final int TOUCHMODE_FULLSCREEN = 1;
 
	/** Constant value for use with setTouchModeAbove(). Denies the SlidingMenu to be opened with a swipe
	 * gesture
	 */
	  /**使用setTouchModeAbove()的常量.不允许SlidingMenu通过一个猛击的手势打开.
	 */
	public static final int TOUCHMODE_NONE = 2;

c.设置显示菜单栏动画效果

// 设置渐入渐出效果的值
        slidingMenu.setFadeDegree(0.35f);
        slidingMenu.setFadeEnabled(true);

2.3将Activity视图添加到SlidingMenu下的上层视图

//使SlidingMenu附加在Activity上
slidingMenu.attachToActivity(this, SlidingMenu.SLIDING_CONTENT);
public void attachToActivity(Activity activity, int slideStyle, boolean actionbarOverlay) {
        if(slideStyle != 0 && slideStyle != 1) {
            throw new IllegalArgumentException("slideStyle must be either SLIDING_WINDOW or SLIDING_CONTENT");
        } else if(this.getParent() != null) {
            throw new IllegalStateException("This SlidingMenu appears to already be attached");
        } else {
            TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{16842836});
            int background = a.getResourceId(0, 0);
            a.recycle();
            switch(slideStyle) {
            case 0:
                this.mActionbarOverlay = false;
                ViewGroup decor = (ViewGroup)activity.getWindow().getDecorView();
                ViewGroup decorChild = (ViewGroup)decor.getChildAt(0);
                decorChild.setBackgroundResource(background);
                decor.removeView(decorChild);
                decor.addView(this);
                this.setContent(decorChild);
                break;
            case 1:
                this.mActionbarOverlay = actionbarOverlay;
                ViewGroup contentParent = (ViewGroup)activity.findViewById(16908290);//获取Activity根视图
                View content = contentParent.getChildAt(0); //获取Activity根视图下的第一个视图(setContentView添加的)
                contentParent.removeView(content);//Activity根视图下的第一个视图(setContentView添加的)
                contentParent.addView(this);将SlidingMenu视图添加到根视图
                this.setContent(content);//将Activity根视图下的第一个视图(setContentView添加的)添加到SlidingMenu下的上层视图
                if(content.getBackground() == null) {
                    content.setBackgroundResource(background);
                }
            }

        }
    }

2.3设置SlidingView菜单栏视图

slidingMenu.setMenu(R.layout.left_menu);
        leftMenu = new LeftMenuFragment();
        getSupportFragmentManager().beginTransaction().add(R.id.left_menu, leftMenu).commit();
        slidingMenu.setSecondaryMenu(R.layout.right_menu);
        rightMenu = new RightMenuFragment();
        getSupportFragmentManager().beginTransaction().add(R.id.right_menu, rightMenu).commit();

在SlidingView将菜单栏视图添加到底层视图

SlidingView
public void setMenu(int res) {
        this.setMenu(LayoutInflater.from(this.getContext()).inflate(res, (ViewGroup)null));
    }

    public void setMenu(View v) {
        this.mViewBehind.setContent(v);
    }

    public void setSecondaryMenu(int res) {
        this.setSecondaryMenu(LayoutInflater.from(this.getContext()).inflate(res, (ViewGroup)null));
    }

    public void setSecondaryMenu(View v) {
        this.mViewBehind.setSecondaryContent(v);
    }

2.4显示SlidingView内容

slidingMenu.showContent();

3.SlidingMenu下实现滑动显示左右菜单栏源码分析

CustomViewAbove.java

主要处理界面的touch事件,解决滑动冲突,控制着整个控件当前的滑动状态,大部分方法属性也传递到slidingmenu.java暴露给了用户;

CustomViewBehind.java

主要处理界面的一些基本属性及状态,如滑动时视差滚动,透明度渐变,对画布的操作,阴影的绘制等,大部分方法也通过slidingmenu.java暴露给了用户,Behind View 中的touch事件也延用了Above View 的touch 事件,相当于统一交予Above View来处理;

CustomViewAbove同时处理CustomViewAbove和CustomViewBehind触摸事件;CustomViewAbove重点主要实现onInterceptTouchEvent()和onTouchEvent()处理滑动事件,onInterceptTouchEvent()主要决定是否进行事件拦截,onTouchEvent()实际处理触摸事件,监听执行视图滑动;

在CustomViewAbove处理事件主要处理按下,滑动,抬起;

    public static final int ACTION_DOWN             = 0; //按下
    public static final int ACTION_UP               = 1; //抬起
    public static final int ACTION_MOVE             = 2; //滑动

3.1按下事件监听

    case MotionEvent.ACTION_DOWN:
                this.completeScroll();
                int index = MotionEventCompat.getActionIndex(ev);
                this.mActivePointerId = MotionEventCompat.getPointerId(ev, index);
                this.mLastMotionX = this.mInitialMotionX = ev.getX();
                break;

this.completeScroll();

如果视图还在滚动,则停止;

this.mActivePointerId = MotionEventCompat.getPointerId(ev, index);

获取第一个按下的触控点(手势)ID,mActivePointerId是记录多点触碰的activity第一个点的id,总之是用来处理多点除控时拖动的稳定性;
this.mLastMotionX = this.mInitialMotionX = ev.getX();

只需要实现水平滚动,记录按下时x轴坐标;

3.2滑动事件监听

case MotionEvent.ACTION_MOVE:
                if(!this.mIsBeingDragged) {
                    this.determineDrag(ev);
                    if(this.mIsUnableToDrag) {
                        return false;
                    }
                }

                if(this.mIsBeingDragged) {
                    activePointerIndex = this.getPointerIndex(ev, this.mActivePointerId);
                    if(this.mActivePointerId != -1) {
                        float x = MotionEventCompat.getX(ev, activePointerIndex);
                        float deltaX = this.mLastMotionX - x;
                        this.mLastMotionX = x;
                        oldScrollX = (float)this.getScrollX();
                        float scrollX = oldScrollX + deltaX;
                        leftBound = (float)this.getLeftBound();
                        float rightBound = (float)this.getRightBound();
                        if(scrollX < leftBound) {
                            scrollX = leftBound;
                        } else if(scrollX > rightBound) {
                            scrollX = rightBound;
                        }

                        this.mLastMotionX += scrollX - (float)((int)scrollX);
                        this.scrollTo((int)scrollX, this.getScrollY());
                        this.pageScrolled((int)scrollX);
                    }
                }
                break;
            case 3:

1)this.determineDrag(ev);

private void determineDrag(MotionEvent ev) {
        //多点触碰的activity第一个点的id
        int activePointerId = this.mActivePointerId;
        //多点触碰的activity第一个点的id对应的索引,代表每个手指的索引值
        int pointerIndex = this.getPointerIndex(ev, activePointerId);
        if(activePointerId != -1 && pointerIndex != -1) {
            float x = MotionEventCompat.getX(ev, pointerIndex);
            float dx = x - this.mLastMotionX;
            float xDiff = Math.abs(dx);
            float y = MotionEventCompat.getY(ev, pointerIndex);
            float dy = y - this.mLastMotionY;
            float yDiff = Math.abs(dy);
            if(xDiff > (float)(this.isMenuOpen()?this.mTouchSlop / 2:this.mTouchSlop) && xDiff > yDiff && this.thisSlideAllowed(dx)) {
                this.startDrag();
                this.mLastMotionX = x;
                this.mLastMotionY = y;
                this.setScrollingCacheEnabled(true);
            } else if(xDiff > (float)this.mTouchSlop) {
                this.mIsUnableToDrag = true;
            }

        }
    }

if(xDiff > (float)(this.isMenuOpen()?this.mTouchSlop / 2:this.mTouchSlop) && xDiff > yDiff && this.thisSlideAllowed(dx))

xDiff/yDiff代表水平和垂直方向移动的距离;

mTouchSlop是系统对于拖动的最低长度判断,即至少移动多少距离,才算是一个拖动的动作;

判断拖动条件:

水平移动距离xDiff大于拖动的最低长度(若菜单栏打开,xDiff大于拖动的最低长度/2),水平方向xDiff移动距离大于垂直方向yDiff移动距离,this.thisSlideAllowed(dx)判断是否允许滑动显示菜单栏(x轴移动的距离(正:向右滑动,负:向左滑动));

//CustomViewAbove
private boolean thisSlideAllowed(float dx) {
        boolean allowed = false;
        if(this.isMenuOpen()) {
            allowed = this.mViewBehind.menuOpenSlideAllowed(dx);
        } else {
            allowed = this.mViewBehind.menuClosedSlideAllowed(dx);
        }

        return allowed;
    }

//CustomViewBehind
public boolean menuClosedSlideAllowed(float dx) {
        return this.mMode == 0?dx > 0.0F:(this.mMode == 1?dx < 0.0F:this.mMode == 2);
    }

    public boolean menuOpenSlideAllowed(float dx) {
        return this.mMode == 0?dx < 0.0F:(this.mMode == 1?dx > 0.0F:this.mMode == 2);
    }

根据滑动方向确认是否需要允许滑动显示菜单栏,例如菜单栏关闭状态判断this.mMode == 0(仅有左侧菜单栏)?dx > 0.0F,dx为正表示向右滑动,允许显示左侧菜单栏;

在x水平移动距离大于y垂直移动距离,同时移动的方向有设置了相应的菜单栏则允许滑动(判断是否允许拖动操作),则判断为触摸事件;

private void determineDrag(MotionEvent ev){

this.startDrag(); //主要设置mIsBeingDragged变量为true,表示滑动准备显示菜单栏
this.mLastMotionX = x;//记录滑动的x轴坐标
this.mLastMotionY = y;//记录滑动的y轴坐标

}

2)this.mIsBeingDragged==true

a.this.scrollTo((int)scrollX, this.getScrollY());

随着手指滑动动态计算滑动位置,执行视图滚动;

public void scrollTo(int x, int y) {
        super.scrollTo(x, y);
        this.mScrollX = (float)x;
        this.mViewBehind.scrollBehindTo(this.mContent, x, y);
        ((SlidingMenu)this.getParent()).manageLayers(this.getPercentOpen());
    }

super.scrollTo(x, y);执行视图滚动,

this.mViewBehind.scrollBehindTo(this.mContent, x, y);调用菜单栏视图执行滚动;

manageLayers是提高性能用的,不深入研究了;

b.this.pageScrolled((int)scrollX);

在SlidingMenu类中会设置监听器,在pageScrolled会调用监听器回调,回传滚动变化;

3.3手势抬起

case MotionEvent.ACTION_UP:
                if(this.mIsBeingDragged) {
                    VelocityTracker velocityTracker = this.mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, (float)this.mMaximumVelocity);
                    int initialVelocity = (int)VelocityTrackerCompat.getXVelocity(velocityTracker, this.mActivePointerId);
                    int scrollX = this.getScrollX();
                    oldScrollX = (float)(scrollX - this.getDestScrollX(this.mCurItem)) / (float)this.getBehindWidth();
                    int activePointerIndex = this.getPointerIndex(ev, this.mActivePointerId);
                    if(this.mActivePointerId != -1) {
                        leftBound = MotionEventCompat.getX(ev, activePointerIndex);
                        int totalDelta = (int)(leftBound - this.mInitialMotionX);
                        int nextPage = this.determineTargetPage(oldScrollX, initialVelocity, totalDelta);
                        this.setCurrentItemInternal(nextPage, true, true, initialVelocity);
                    } else {
                        this.setCurrentItemInternal(this.mCurItem, true, true, initialVelocity);
                    }

                    this.mActivePointerId = -1;
                    this.endDrag();
                } else if(this.mQuickReturn && this.mViewBehind.menuTouchInQuickReturn(this.mContent, this.mCurItem, ev.getX() + this.mScrollX)) {
                    this.setCurrentItem(1);
                    this.endDrag();
                }
                break;

手势抬起时检测是否是显示菜单栏滑动操作;

主要包含两部分实现,一个是计算滑动以后要显示页号(0:表示左菜单,1:表示当前主页,2:表示右侧菜单),另一部分是执行页面滚动,完成目标页号的显示;

a.int nextPage = this.determineTargetPage(oldScrollX, initialVelocity, totalDelta);

private int determineTargetPage(float pageOffset, int velocity, int deltaX) {
        int targetPage = this.mCurItem;
        if(Math.abs(deltaX) > this.mFlingDistance && Math.abs(velocity) > this.mMinimumVelocity) {
            if(velocity > 0 && deltaX > 0) {
                --targetPage;
            } else if(velocity < 0 && deltaX < 0) {
                ++targetPage;
            }
        } else {
            targetPage = Math.round((float)this.mCurItem + pageOffset);
        }

        return targetPage;
    }

pageOffset:表示页面偏移比例(范围:-1-1),负值表示表示向右滑动,正值表示向左滑动;

velocity:表示滑动速度;

deltaX:总的滑动距离;

条件判断根据距离和速度判断,若移动的那个的距离大于this.mFlingDistance(float density = context.getResources().getDisplayMetrics().density;
this.mFlingDistance = (int)(25.0F * density);)同时滑动速度大于最小滑动速度,则根据滑动速度和具体计算目标页号,同时为正,显示左侧,同理相反;

否则采用四舍五入方式,例如pageOffset=-0.7;则1+(-0.7)=0.3,目标页号为0;

b.this.setCurrentItemInternal(nextPage, true, true, initialVelocity);

实际执行滚动显示目标页面;

void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
        if(!always && this.mCurItem == item) {
            this.setScrollingCacheEnabled(false);
        } else {
            item = this.mViewBehind.getMenuPage(item);
            boolean dispatchSelected = this.mCurItem != item;
            this.mCurItem = item;
            int destX = this.getDestScrollX(this.mCurItem);
            if(dispatchSelected && this.mOnPageChangeListener != null) {
                this.mOnPageChangeListener.onPageSelected(item);
            }

            if(dispatchSelected && this.mInternalPageChangeListener != null) {
                this.mInternalPageChangeListener.onPageSelected(item);
            }

            if(smoothScroll) {
                this.smoothScrollTo(destX, 0, velocity);
            } else {
                this.completeScroll();
                this.scrollTo(destX, 0);
            }

        }
    }

a.页面变化监听;

b.滚动到目标页面

this.smoothScrollTo(destX, 0, velocity);

void smoothScrollTo(int x, int y, int velocity) {
        if(this.getChildCount() == 0) {
            this.setScrollingCacheEnabled(false);
        } else {
            int sx = this.getScrollX();
            int sy = this.getScrollY();
            int dx = x - sx;
            int dy = y - sy;
            if(dx == 0 && dy == 0) {
                this.completeScroll();
                if(this.isMenuOpen()) {
                    if(this.mOpenedListener != null) {
                        this.mOpenedListener.onOpened();
                    }
                } else if(this.mClosedListener != null) {
                    this.mClosedListener.onClosed();
                }

            } else {
                this.setScrollingCacheEnabled(true);
                this.mScrolling = true;
                int width = this.getBehindWidth();
                int halfWidth = width / 2;
                float distanceRatio = Math.min(1.0F, 1.0F * (float)Math.abs(dx) / (float)width);
                float distance = (float)halfWidth + (float)halfWidth * this.distanceInfluenceForSnapDuration(distanceRatio);
                int duration = false;
                velocity = Math.abs(velocity);
                int duration;
                if(velocity > 0) {
                    duration = 4 * Math.round(1000.0F * Math.abs(distance / (float)velocity));
                } else {
                    float pageDelta = (float)Math.abs(dx) / (float)width;
                    duration = (int)((pageDelta + 1.0F) * 100.0F);
                    duration = 600;
                }

                duration = Math.min(duration, 600);
                this.mScroller.startScroll(sx, sy, dx, dy, duration);
                this.invalidate();
            }
        }
    }

判断条件若当前需要滚动的距离为0,则直接通知菜单栏打开或者额关闭;

否则根据duration执行滚动:

this.mScroller.startScroll(sx, sy, dx, dy, duration);
this.invalidate();

3.4事件的分发和拦截

public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(!this.mEnabled) {
            return false;
        } else {
            int action = ev.getAction() & 255;
            if(action != 3 && action != 1 && (action == 0 || !this.mIsUnableToDrag)) {
                switch(action) {
                case 0:
                    int index = MotionEventCompat.getActionIndex(ev);
                    this.mActivePointerId = MotionEventCompat.getPointerId(ev, index);
                    if(this.mActivePointerId != -1) {
                        this.mLastMotionX = this.mInitialMotionX = MotionEventCompat.getX(ev, index);
                        this.mLastMotionY = MotionEventCompat.getY(ev, index);
                        if(this.thisTouchAllowed(ev)) {
                            this.mIsBeingDragged = false;
                            this.mIsUnableToDrag = false;
                            if(this.isMenuOpen() && this.mViewBehind.menuTouchInQuickReturn(this.mContent, this.mCurItem, ev.getX() + this.mScrollX)) {
                                this.mQuickReturn = true;
                            }
                        } else {
                            this.mIsUnableToDrag = true;
                        }
                    }
                    break;
                case 2:
                    this.determineDrag(ev);
                    break;
                case 6:
                    this.onSecondaryPointerUp(ev);
                }

                if(!this.mIsBeingDragged) {
                    if(this.mVelocityTracker == null) {
                        this.mVelocityTracker = VelocityTracker.obtain();
                    }

                    this.mVelocityTracker.addMovement(ev);
                }

                return this.mIsBeingDragged || this.mQuickReturn;
            } else {
                this.endDrag();
                return false;
            }
        }
    }

onInterceptTouchEvent返回true表示当前视图onTouchEvent处理触摸事件,false表示继续向下传递;

在菜单打开的时候要进行判断:

 if(this.isMenuOpen() && this.mViewBehind.menuTouchInQuickReturn(this.mContent, this.mCurItem, ev.getX() + this.mScrollX)) {
                                this.mQuickReturn = true;
                            }
如果点击在主体部分,则自动收回菜单,即让主体部分消费这个touch事件;
如果此时点击在菜单部分,那above中应该不消费此事件,要将其传给behind部分,然后让behind中的listview或者button等自定义设置的控件去获取处理touch事件;

3.5当前页面点击返回按钮时关闭菜单栏

 @Override
    public void onBackPressed() {
        if (slidingMenu.isMenuShowing() || slidingMenu.isSecondaryMenuShowing()) {
            slidingMenu.showContent();
        } else {
            if (System.currentTimeMillis() - mLastClickTime <= 2000L) {
                super.onBackPressed();
            } else {
                mLastClickTime = System.currentTimeMillis();
                Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show();
            }

        }

    }

4.CustomViewBehind

CustomViewBehind主要实现菜单栏的滚动显示/隐藏和菜单栏的阴影绘制等;

4.1菜单栏的滚动显示/隐藏

public void scrollBehindTo(View content, int x, int y) {
        int vis = 0;
        if(this.mMode == 0) {
            if(x >= content.getLeft()) {
                vis = 4;
            }

            this.scrollTo((int)((float)(x + this.getBehindWidth()) * this.mScrollScale), y);
        } else if(this.mMode == 1) {
            if(x <= content.getLeft()) {
                vis = 4;
            }

            this.scrollTo((int)((float)(this.getBehindWidth() - this.getWidth()) + (float)(x - this.getBehindWidth()) * this.mScrollScale), y);
        } else if(this.mMode == 2) {
            this.mContent.setVisibility(x >= content.getLeft()?4:0);
            this.mSecondaryContent.setVisibility(x <= content.getLeft()?4:0);
            vis = x == 0?4:0;
            if(x <= content.getLeft()) {
                this.scrollTo((int)((float)(x + this.getBehindWidth()) * this.mScrollScale), y);
            } else {
                this.scrollTo((int)((float)(this.getBehindWidth() - this.getWidth()) + (float)(x - this.getBehindWidth()) * this.mScrollScale), y);
            }
        }

        if(vis == 4) {
            Log.v("CustomViewBehind", "behind INVISIBLE");
        }

        this.setVisibility(vis);
    }

4.1.1只分析LEFT的情况(请他情况同理)

检测滚动的x坐标大于主视图CustomViewAbove左侧则显示菜单栏视图;

if(x >= content.getLeft()) {
                vis = 4;
            }

实现菜单栏内滚动

this.scrollTo((int)((float)(x + this.getBehindWidth()) * this.mScrollScale), y);
比如菜单的width即getBehindWidth的宽度为300
那从菜单关闭状态一直滚到菜单完全打开状态,above主体内容x轴上的scroll变化就是0 ~ -300
此时如果mScrollScale即滚动比例为0.3
那按照上面scrollBehindTo中的算法, behind菜单部分x轴上的scroll变化就是
(0+300)*0.3 ~ (-300+300)*0.3即100~0

如果比例换成0.6,那behind的x变化就是180~0

极端情况下
1.mScrollScale=0, behind的x变化为 0~0, 此时会发现behind菜单部分在打开关闭的过程中无任何滚动
2.mScrollScale=1,behind的换标为300~0, 此时的效果就是主体和菜单紧挨着以同一个速度滚动

注意,这里的x为滚动的偏移量,即scrollTo使用的参数,
比如scrollView中,scrollTo(0, 100)会往下滚动到y偏移量100的位置,
此时scrollView里面的内容视觉上看其实是向上移了~
这里同理
虽然朝右拖动的时候view看上去是朝右运动,但x轴上滚动偏移量是减少的

4.1.2在CustomViewAbove重写dispatchDraw方法,调用CustomViewAbove绘制阴影,淡入和选择器方法实现绘制

CustomViewAbove

protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        this.mViewBehind.drawShadow(this.mContent, canvas);
        this.mViewBehind.drawFade(this.mContent, canvas, this.getPercentOpen());
        this.mViewBehind.drawSelector(this.mContent, canvas, this.getPercentOpen());
    }

CustomViewAbove

public void drawShadow(View content, Canvas canvas) {
        if(this.mShadowDrawable != null && this.mShadowWidth > 0) {
            int left = 0;
            if(this.mMode == 0) {
                left = content.getLeft() - this.mShadowWidth;
            } else if(this.mMode == 1) {
                left = content.getRight();
            } else if(this.mMode == 2) {
                if(this.mSecondaryShadowDrawable != null) {
                    left = content.getRight();
                    this.mSecondaryShadowDrawable.setBounds(left, 0, left + this.mShadowWidth, this.getHeight());
                    this.mSecondaryShadowDrawable.draw(canvas);
                }

                left = content.getLeft() - this.mShadowWidth;
            }

            this.mShadowDrawable.setBounds(left, 0, left + this.mShadowWidth, this.getHeight());
            this.mShadowDrawable.draw(canvas);
        }
    }

this.mShadowDrawable.draw(canvas);将阴影Drawable绘制到画布上;

public void drawFade(View content, Canvas canvas, float openPercent) {
        if(this.mFadeEnabled) {
            int alpha = (int)(this.mFadeDegree * 255.0F * Math.abs(1.0F - openPercent));
            this.mFadePaint.setColor(Color.argb(alpha, 0, 0, 0));
            int left = 0;
            int right = 0;
            if(this.mMode == 0) {
                left = content.getLeft() - this.getBehindWidth();
                right = content.getLeft();
            } else if(this.mMode == 1) {
                left = content.getRight();
                right = content.getRight() + this.getBehindWidth();
            } else if(this.mMode == 2) {
                left = content.getLeft() - this.getBehindWidth();
                right = content.getLeft();
                canvas.drawRect((float)left, 0.0F, (float)right, (float)this.getHeight(), this.mFadePaint);
                left = content.getRight();
                right = content.getRight() + this.getBehindWidth();
            }

            canvas.drawRect((float)left, 0.0F, (float)right, (float)this.getHeight(), this.mFadePaint);
        }
    }

在画布上绘制阴影效果;

参考:

https://blog.youkuaiyun.com/u011494050/article/details/43531087

https://github.com/jfeinstein10/SlidingMenu

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值