Android · 2015年3月3日 0

ym——Android仿QQ5.0侧滑菜单ResideMenu源码分析

AndroidResideMenu

 

\

先看看如何使用:

把项目源码下载下来导入工程,可以看到

\

ResideMenu为引用工程,再看看如何使用这个引用工程来构建出ResideMenu,

1.先new一个ResideMenu对象

 

1
resideMenu = new ResideMenu(this);

2.设置它的背景图片

 

 

1
resideMenu.setBackground(R.drawable.menu_background);

3.绑定当前Activity

 

 

1
resideMenu.attachToActivity(this);

4.设置监听

 

 

1
resideMenu.setMenuListener(menuListener);

可以监听菜单打开和关闭状态

 

 

1
2
3
4
5
6
7
8
9
10
11
private ResideMenu.OnMenuListener menuListener = new ResideMenu.OnMenuListener() {
    @Override
    public void openMenu() {
        Toast.makeText(mContext, Menu is opened!, Toast.LENGTH_SHORT).show();
    }
    @Override
    public void closeMenu() {
        Toast.makeText(mContext, Menu is closed!, Toast.LENGTH_SHORT).show();
    }
};

5.设置内容缩放比例(0.1~1f)

 

 

1
2
//valid scale factor is between 0.0f and 1.0f. leftmenu'width is 150dip.
        resideMenu.setScaleValue(0.6f);

6.创建子菜单

 

 

1
2
3
4
5
// create menu items;
       itemHome     = new ResideMenuItem(this, R.drawable.icon_home,     Home);
       itemProfile  = new ResideMenuItem(this, R.drawable.icon_profile,  Profile);
       itemCalendar = new ResideMenuItem(this, R.drawable.icon_calendar, Calendar);
       itemSettings = new ResideMenuItem(this, R.drawable.icon_settings, Settings);

7.设置点击事件及将刚创建的子菜单添加到侧换菜单中(可以看到它是通过常量来控制子菜单的添加位置)

 

 

1
2
3
4
5
6
7
8
9
itemHome.setOnClickListener(this);
        itemProfile.setOnClickListener(this);
        itemCalendar.setOnClickListener(this);
        itemSettings.setOnClickListener(this);
        resideMenu.addMenuItem(itemHome, ResideMenu.DIRECTION_LEFT);
        resideMenu.addMenuItem(itemProfile, ResideMenu.DIRECTION_LEFT);
        resideMenu.addMenuItem(itemCalendar, ResideMenu.DIRECTION_RIGHT);
        resideMenu.addMenuItem(itemSettings, ResideMenu.DIRECTION_RIGHT);

8.设置title按钮的点击事件,设置左右菜单的开关

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// You can disable a direction by setting ->
        // resideMenu.setSwipeDirectionDisable(ResideMenu.DIRECTION_RIGHT);
        findViewById(R.id.title_bar_left_menu).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                resideMenu.openMenu(ResideMenu.DIRECTION_LEFT);
            }
        });
        findViewById(R.id.title_bar_right_menu).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                resideMenu.openMenu(ResideMenu.DIRECTION_RIGHT);
            }
        });

9.还重写了dispatchTouchEvent

 

 

1
2
3
4
@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return resideMenu.dispatchTouchEvent(ev);
    }

10.菜单关闭方法

 

 

1
resideMenu.closeMenu();

 

11.屏蔽菜单方法

 

1
2
// You can disable a direction by setting ->
        // resideMenu.setSwipeDirectionDisable(ResideMenu.DIRECTION_RIGHT);

 

使用方法已经说完了,接下来,看看它的源码,先看看源码的项目结构。

\

 

很多人初学者都曾纠结,看源码,如何从何看起,我个人建议从上面使用的顺序看起,并且在看的时候要带个问题去看去思考,这样更容易理解。

上面的第一步是,创建ResideMenu对象,我们就看看ResideMenu的构造。

 

1
2
3
4
public ResideMenu(Context context) {
        super(context);
        initViews(context);
    }

从上面代码,看到构造里面就一个初始化view,思考问题:如何初始化view及初始化了什么view。

 

 

1
2
3
4
5
6
7
8
9
10
11
private void initViews(Context context){
        LayoutInflater inflater = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.residemenu, this);
        scrollViewLeftMenu = (ScrollView) findViewById(R.id.sv_left_menu);
        scrollViewRightMenu = (ScrollView) findViewById(R.id.sv_right_menu);
        imageViewShadow = (ImageView) findViewById(R.id.iv_shadow);
        layoutLeftMenu = (LinearLayout) findViewById(R.id.layout_left_menu);
        layoutRightMenu = (LinearLayout) findViewById(R.id.layout_right_menu);
        imageViewBackground = (ImageView) findViewById(R.id.iv_background);
    }

原理分析:从上面的代码可以看到,加载了一个residemenu的布局,先看布局

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--?xml version=1.0 encoding=utf-8?-->
<framelayout android:layout_height="match_parent" android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android">
    <imageview android:adjustviewbounds="true" android:id="@+id/iv_background" android:layout_height="match_parent/" android:layout_width="match_parent" android:scaletype="centerCrop">
    <imageview android:background="@drawable/shadow" android:id="@+id/iv_shadow" android:layout_height="fill_parent" android:layout_width="fill_parent" android:scaletype="fitXY/">
    <scrollview android:id="@+id/sv_left_menu" android:layout_height="fill_parent" android:layout_width="150dp" android:paddingleft="30dp" android:scrollbars="none">
        <linearlayout android:id="@+id/layout_left_menu" android:layout_gravity="center_vertical" android:layout_height="wrap_content" android:layout_width="wrap_content" android:orientation="vertical">
        </linearlayout>
    </scrollview>
    <scrollview android:id="@+id/sv_right_menu" android:layout_gravity="right" android:layout_height="fill_parent" android:layout_width="150dp" android:paddingright="30dp" android:scrollbars="none">
        <linearlayout android:gravity="right" android:id="@+id/layout_right_menu" android:layout_gravity="center_vertical" android:layout_height="wrap_content" android:layout_width="wrap_content" android:orientation="vertical">
        </linearlayout>
    </scrollview>
</imageview></imageview></framelayout>

布局显示效果

 

\
从布局文件,以及显示效果我们可以看到,它是一个帧布局,第一个ImageView是背景,第二个ImageView是.9的阴影效果的图片(看下面的图),

两个(ScrollView包裹着一个LinerLayout),可以从上面图看到结构分别是左菜单和右菜单

1

/

 

1.初始化布局以及布局文件分析完毕,2.接下来是设置背景图,初始化view的时候就已经拿到了背景控件,所以设置背景图也是非常好实现的事情了。

 

1
2
3
public void setBackground(int imageResrouce){
       imageViewBackground.setImageResource(imageResrouce);
   }

3.绑定activity,思考问题:它做了什么?

 

 

1
2
3
4
5
6
7
8
9
10
11
/**
  * use the method to set up the activity which residemenu need to show;
  *
  * @param activity
  */
 public void attachToActivity(Activity activity){
     initValue(activity);
     setShadowAdjustScaleXByOrientation();
     viewDecor.addView(this, 0);
     setViewPadding();
 }

原理分析:绑定activity做了4件事情,分别是:

 

1.初始化参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void initValue(Activity activity){
    this.activity   = activity;
    leftMenuItems   = new ArrayList<residemenuitem>();
    rightMenuItems  = new ArrayList<residemenuitem>();
    ignoredViews    = new ArrayList<view>();
    viewDecor = (ViewGroup) activity.getWindow().getDecorView();
    viewActivity = new TouchDisableView(this.activity);
    View mContent   = viewDecor.getChildAt(0);
    viewDecor.removeViewAt(0);
    viewActivity.setContent(mContent);
    addView(viewActivity);
    ViewGroup parent = (ViewGroup) scrollViewLeftMenu.getParent();
    parent.removeView(scrollViewLeftMenu);
    parent.removeView(scrollViewRightMenu);
}</view></residemenuitem></residemenuitem>

 

2.正对横竖屏缩放比例进行调整

1
2
3
4
5
6
7
8
9
10
private void setShadowAdjustScaleXByOrientation(){
      int orientation = getResources().getConfiguration().orientation;
      if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
          shadowAdjustScaleX = 0.034f;
          shadowAdjustScaleY = 0.12f;
      } else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
          shadowAdjustScaleX = 0.06f;
          shadowAdjustScaleY = 0.07f;
      }
  }

 

3.添加当前view

1
viewDecor.addView(this, 0);

 

4.设置view边距

1
2
3
4
5
6
7
8
9
10
/**
     * we need the call the method before the menu show, because the
     * padding of activity can't get at the moment of onCreateView();
     */
    private void setViewPadding(){
        this.setPadding(viewActivity.getPaddingLeft(),
                viewActivity.getPaddingTop(),
                viewActivity.getPaddingRight(),
                viewActivity.getPaddingBottom());
    }

4.设置监听,思考问题:它什么时候调用监听,原理分析:动画监听开始执行动画掉哦那个openMenu动画结束调用closeMenu,从此我们可以想到,但它调用openMenu(int direction)和closeMenu()都会设置这个监听。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private Animator.AnimatorListener animationListener = new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
            if (isOpened()){
                showScrollViewMenu();
                if (menuListener != null)
                    menuListener.openMenu();
            }
        }
        @Override
        public void onAnimationEnd(Animator animation) {
            // reset the view;
            if(isOpened()){
                viewActivity.setTouchDisable(true);
                viewActivity.setOnClickListener(viewActivityOnClickListener);
            }else{
                viewActivity.setTouchDisable(false);
                viewActivity.setOnClickListener(null);
                hideScrollViewMenu();
                if (menuListener != null)
                    menuListener.closeMenu();
            }
        }
        @Override
        public void onAnimationCancel(Animator animation) {
        }
        @Override
        public void onAnimationRepeat(Animator animation) {
        }
    };

5.设置内容缩放比例(0.1~1f),细心的同学会发现在当缩完成后还可以在往里面拉到更小,有种弹性的感觉,挺有趣的。但是有些人的需求不想要有这种弹性效果,我们可以通过修改源码修改这个弹性效果,找到getTargetScale这个方法,修改下面0.5这个数值。使用时设置了0.6的缩放比例,默认下面的弹性参数是0.5所以我们当缩完成后还可以在往里面拉0.1的比例。

 

1
2
3
4
5
6
7
8
9
private float getTargetScale(float currentRawX){
    float scaleFloatX = ((currentRawX - lastRawX) / getScreenWidth()) * 0.75f;
    scaleFloatX = scaleDirection == DIRECTION_RIGHT ? - scaleFloatX : scaleFloatX;
    float targetScale = ViewHelper.getScaleX(viewActivity) - scaleFloatX;
    targetScale = targetScale > 1.0f ? 1.0f : targetScale;
    targetScale = targetScale < 0.5f ? 0.5f : targetScale;
    return targetScale;
}

 

默认缩放比例:

1
2
//valid scale factor is between 0.0f and 1.0f.
   private float mScaleValue = 0.5f;
1
AnimatorSet scaleDown_activity = buildScaleDownAnimation(viewActivity, mScaleValue, mScaleValue);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
     * a helper method to build scale down animation;
     *
     * @param target
     * @param targetScaleX
     * @param targetScaleY
     * @return
     */
    private AnimatorSet buildScaleDownAnimation(View target,float targetScaleX,float targetScaleY){
        AnimatorSet scaleDown = new AnimatorSet();
        scaleDown.playTogether(
                ObjectAnimator.ofFloat(target, scaleX, targetScaleX),
                ObjectAnimator.ofFloat(target, scaleY, targetScaleY)
        );
        scaleDown.setInterpolator(AnimationUtils.loadInterpolator(activity,
                android.R.anim.decelerate_interpolator));
        scaleDown.setDuration(250);
        return scaleDown;
    }

6.创建子菜单,看下子菜单的构造,我们通过上面的学习,原理分析:我们可以猜测到,无非就是加载布局设置内容

 

1
2
3
4
5
6
7
8
9
10
11
12
13
public ResideMenuItem(Context context, int icon, String title) {
        super(context);
        initViews(context);
        iv_icon.setImageResource(icon);
        tv_title.setText(title);
    }
    private void initViews(Context context){
        LayoutInflater inflater=(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.residemenu_item, this);
        iv_icon = (ImageView) findViewById(R.id.iv_icon);
        tv_title = (TextView) findViewById(R.id.tv_title);
    }

布局文件:

 

1
2
3
4
5
6
7
8
9
<!--?xml version=1.0 encoding=utf-8?-->
<linearlayout android:gravity="center_vertical" android:layout_height="wrap_content" android:layout_width="match_parent" android:orientation="horizontal" android:paddingtop="30dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <imageview android:id="@+id/iv_icon/" android:layout_height="30dp" android:layout_width="30dp" android:scaletype="centerCrop">
    <textview android:id="@+id/tv_title/" android:layout_height="wrap_content" android:layout_marginleft="10dp" android:layout_width="match_parent" android:textcolor="@android:color/white" android:textsize="18sp">
</textview></imageview></linearlayout>

 

显示效果图:

\

7.子菜单添加到侧换菜单中(可以看到它是通过常量来控制子菜单的添加位置)原理分析:根据不同的常量来区分添加不同菜单的子菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
    * add a single items;
    *
    * @param menuItem
    * @param direction
    */
   public void addMenuItem(ResideMenuItem menuItem, int direction){
       if (direction == DIRECTION_LEFT){
           this.leftMenuItems.add(menuItem);
           layoutLeftMenu.addView(menuItem);
       }else{
           this.rightMenuItems.add(menuItem);
           layoutRightMenu.addView(menuItem);
       }
   }

8.设置title按钮的点击事件,设置左右菜单的开关,原理分析:先设置了缩放方向然后在设置动画,正如我们上面想的一样还设置了动画监听。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * show the reside menu;
 */
public void openMenu(int direction){
    setScaleDirection(direction);
    isOpened = true;
    AnimatorSet scaleDown_activity = buildScaleDownAnimation(viewActivity, mScaleValue, mScaleValue);
    AnimatorSet scaleDown_shadow = buildScaleDownAnimation(imageViewShadow,
            mScaleValue + shadowAdjustScaleX, mScaleValue + shadowAdjustScaleY);
    AnimatorSet alpha_menu = buildMenuAnimation(scrollViewMenu, 1.0f);
    scaleDown_shadow.addListener(animationListener);
    scaleDown_activity.playTogether(scaleDown_shadow);
    scaleDown_activity.playTogether(alpha_menu);
    scaleDown_activity.start();
}

设置缩放方向及计算x,y轴位置。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void setScaleDirection(int direction){
    int screenWidth = getScreenWidth();
    float pivotX;
    float pivotY = getScreenHeight() * 0.5f;
    if (direction == DIRECTION_LEFT){
        scrollViewMenu = scrollViewLeftMenu;
        pivotX  = screenWidth * 1.5f;
    }else{
        scrollViewMenu = scrollViewRightMenu;
        pivotX  = screenWidth * -0.5f;
    }
    ViewHelper.setPivotX(viewActivity, pivotX);
    ViewHelper.setPivotY(viewActivity, pivotY);
    ViewHelper.setPivotX(imageViewShadow, pivotX);
    ViewHelper.setPivotY(imageViewShadow, pivotY);
    scaleDirection = direction;
}

9.重写dispatchTouchEvent,问题思考:如何到根据手指滑动自动缩放

 

如果还不了解,dispatchTouchEvent这个函数如何调用?什么时候调用?请先看看http://blog.csdn.net/cym492224103/article/details/39179311

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    float currentActivityScaleX = ViewHelper.getScaleX(viewActivity);
    if (currentActivityScaleX == 1.0f)
        setScaleDirectionByRawX(ev.getRawX());
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            lastActionDownX = ev.getX();
            lastActionDownY = ev.getY();
            isInIgnoredView = isInIgnoredView(ev) && !isOpened();
            pressedState    = PRESSED_DOWN;
            break;
        case MotionEvent.ACTION_MOVE:
            if (isInIgnoredView || isInDisableDirection(scaleDirection))
                break;
            if(pressedState != PRESSED_DOWN &&
                    pressedState != PRESSED_MOVE_HORIZANTAL)
                break;
            int xOffset = (int) (ev.getX() - lastActionDownX);
            int yOffset = (int) (ev.getY() - lastActionDownY);
            if(pressedState == PRESSED_DOWN) {
                if(yOffset > 25 || yOffset < -25) {
                    pressedState = PRESSED_MOVE_VERTICAL;
                    break;
                }
                if(xOffset < -50 || xOffset > 50) {
                    pressedState = PRESSED_MOVE_HORIZANTAL;
                    ev.setAction(MotionEvent.ACTION_CANCEL);
                }
            } else if(pressedState == PRESSED_MOVE_HORIZANTAL) {
                if (currentActivityScaleX < 0.95)
                    showScrollViewMenu();
                float targetScale = getTargetScale(ev.getRawX());
                ViewHelper.setScaleX(viewActivity, targetScale);
                ViewHelper.setScaleY(viewActivity, targetScale);
                ViewHelper.setScaleX(imageViewShadow, targetScale + shadowAdjustScaleX);
                ViewHelper.setScaleY(imageViewShadow, targetScale + shadowAdjustScaleY);
                ViewHelper.setAlpha(scrollViewMenu, (1 - targetScale) * 2.0f);
                lastRawX = ev.getRawX();
                return true;
            }
            break;
        case MotionEvent.ACTION_UP:
            if (isInIgnoredView) break;
            if (pressedState != PRESSED_MOVE_HORIZANTAL) break;
            pressedState = PRESSED_DONE;
            if (isOpened()){
                if (currentActivityScaleX > 0.56f)
                    closeMenu();
                else
                    openMenu(scaleDirection);
            }else{
                if (currentActivityScaleX < 0.94f){
                    openMenu(scaleDirection);
                }else{
                    closeMenu();
                }
            }
            break;
    }
    lastRawX = ev.getRawX();
    return super.dispatchTouchEvent(ev);
}

上面代码量有点多,看上去有点晕,接下来我们来分别从按下、移动、放开、来原理分析:

 

MotionEvent.ACTION_DOWN:

记录了X,Y轴的坐标点,判断是否打开,设置了按下的状态为PRESSED_DOWN

MotionEvent.ACTION_MOVE:

拿到当前X,Y减去DOWN下记录下来的X,Y,这样得到了移动的X,Y,

然后判断如果如果移动的X,Y大于25或者小于-25就改变按下状态为PRESSED_MOVE_VERTICAL

如果移动的X,Y大于50或者小于-50就改变状态为PRESSED_MOVE_HORIZANTAL

状态为PRESSED_MOVE_HORIZANTAL就改变菜单主视图内容以及阴影图片大小,在改变的同时还设置了当前菜单的透明度。

MotionEvent.ACTION_UP:

判断是否菜单是否打开状态,在获取当前缩放的X比例,

判断比例小于0.56f,则关闭菜单,反正开启菜单。

看完后,我们在回去看看代码,就会发现其实也不过如此~!

10.菜单关闭方法,同样也设置了动画监听之前的想法也是成立的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
    * close the reslide menu;
    */
   public void closeMenu(){
       isOpened = false;
       AnimatorSet scaleUp_activity = buildScaleUpAnimation(viewActivity, 1.0f, 1.0f);
       AnimatorSet scaleUp_shadow = buildScaleUpAnimation(imageViewShadow, 1.0f, 1.0f);
       AnimatorSet alpha_menu = buildMenuAnimation(scrollViewMenu, 0.0f);
       scaleUp_activity.addListener(animationListener);
       scaleUp_activity.playTogether(scaleUp_shadow);
       scaleUp_activity.playTogether(alpha_menu);
       scaleUp_activity.start();
   }

11.屏蔽菜单方法

 

1
2
3
public void setSwipeDirectionDisable(int direction){
        disabledSwipeDirection.add(direction);
    }
1
2
3
private boolean isInDisableDirection(int direction){
        return disabledSwipeDirection.contains(direction);
    }

原理分析:在重写dispatchTouchEvent的时候,细心的同学应该会看到,ACTION_MOVE下面有个判断

 

1
if (isInIgnoredView || isInDisableDirection(scaleDirection))

如果这个方向的菜单被屏蔽了,就滑不出来了。

 

最后我们会发现我们一直都没说到TouchDisableView,其实initValue的时候就初始化了,它就是viewActivity,是我们的内容视图。

\

我们来看看它做了什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = getDefaultSize(0, widthMeasureSpec);
        int height = getDefaultSize(0, heightMeasureSpec);
        setMeasuredDimension(width, height);
        final int contentWidth = getChildMeasureSpec(widthMeasureSpec, 0, width);
        final int contentHeight = getChildMeasureSpec(heightMeasureSpec, 0, height);
        mContent.measure(contentWidth, contentHeight);
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int width = r - l;
        final int height = b - t;
        mContent.layout(0, 0, width, height);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mTouchDisabled;
    }
    void setTouchDisable(boolean disableTouch) {
        mTouchDisabled = disableTouch;
    }
    boolean isTouchDisabled() {
        return mTouchDisabled;
    }

动态设置宽高,设置事件是否传递下去的flag。

 

Share this: