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。