您的位置:首页 > 移动开发 > Android开发

Android仿QQ5.0侧滑菜单ResideMenu的使用和源码分析

2015-11-03 15:14 465 查看
本文出自Cym的博客(http://blog.csdn.net/cym492224103

ResideMenu

github:https://github.com/SpecialCyCi/AndroidResideMenu

csdn:http://download.csdn.net/detail/cym492224103/7887801



先看看如何使用:

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



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

1.先new一个ResideMenu对象

[java] view
plaincopyprint?

resideMenu = new ResideMenu(this);

2.设置它的背景图片

[java] view
plaincopyprint?

resideMenu.setBackground(R.drawable.menu_background);

3.绑定当前Activity

[java] view
plaincopyprint?

resideMenu.attachToActivity(this);

4.设置监听

[java] view
plaincopyprint?

resideMenu.setMenuListener(menuListener);

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

[java] view
plaincopyprint?

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)

[java] view
plaincopyprint?

//valid scale factor is between 0.0f and 1.0f. leftmenu'width is 150dip.

resideMenu.setScaleValue(0.6f);

6.创建子菜单

[java] view
plaincopyprint?

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

[java] view
plaincopyprint?

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按钮的点击事件,设置左右菜单的开关

[java] view
plaincopyprint?

// 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

[java] view
plaincopyprint?

@Override

public boolean dispatchTouchEvent(MotionEvent ev) {

return resideMenu.dispatchTouchEvent(ev);

}

10.菜单关闭方法

[java] view
plaincopyprint?

resideMenu.closeMenu();

11.屏蔽菜单方法

[java] view
plaincopyprint?

// You can disable a direction by setting ->

// resideMenu.setSwipeDirectionDisable(ResideMenu.DIRECTION_RIGHT);

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



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

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

[java] view
plaincopyprint?

public ResideMenu(Context context) {

super(context);

initViews(context);

}

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

[java] view
plaincopyprint?

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的布局,先看布局

[java] view
plaincopyprint?

<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="match_parent">

<ImageView

android:id="@+id/iv_background"

android:adjustViewBounds="true"

android:scaleType="centerCrop"

android:layout_width="match_parent"

android:layout_height="match_parent"/>

<ImageView

android:id="@+id/iv_shadow"

android:background="@drawable/shadow"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:scaleType="fitXY"/>

<ScrollView

android:id="@+id/sv_left_menu"

android:scrollbars="none"

android:paddingLeft="30dp"

android:layout_width="150dp"

android:layout_height="fill_parent">

<LinearLayout

android:id="@+id/layout_left_menu"

android:orientation="vertical"

android:layout_gravity="center_vertical"

android:layout_width="wrap_content"

android:layout_height="wrap_content">

</LinearLayout>

</ScrollView>

<ScrollView

android:id="@+id/sv_right_menu"

android:scrollbars="none"

android:paddingRight="30dp"

android:layout_width="150dp"

android:layout_height="fill_parent"

android:layout_gravity="right">

<LinearLayout

android:id="@+id/layout_right_menu"

android:orientation="vertical"

android:layout_gravity="center_vertical"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:gravity="right">

</LinearLayout>

</ScrollView>

</FrameLayout>

布局显示效果



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

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

[java] view
plaincopyprint?

<img src="http://img.blog.csdn.net/20140910100807704?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY3ltNDkyMjI0MTAz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" style="font-family: Arial; background-color: rgb(255, 255, 255);" alt="" />

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

[java] view
plaincopyprint?

public void setBackground(int imageResrouce){

imageViewBackground.setImageResource(imageResrouce);

}

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

[java] view
plaincopyprint?

/**

* 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.初始化参数:

[java] view
plaincopyprint?

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);

}

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

[java] view
plaincopyprint?

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

[java] view
plaincopyprint?

viewDecor.addView(this, 0);

4.设置view边距

[java] view
plaincopyprint?

/**

* 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()都会设置这个监听。

[java] view
plaincopyprint?

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的比例。

[java] view
plaincopyprint?

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;

}

默认缩放比例:

[java] view
plaincopyprint?

//valid scale factor is between 0.0f and 1.0f.

private float mScaleValue = 0.5f;

[java] view
plaincopyprint?

AnimatorSet scaleDown_activity = buildScaleDownAnimation(viewActivity, mScaleValue, mScaleValue);

[java] view
plaincopyprint?

/**

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

[java] view
plaincopyprint?

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);

}

布局文件:

[html] view
plaincopyprint?

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="horizontal"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:gravity="center_vertical"

android:paddingTop="30dp">

<ImageView

android:layout_width="30dp"

android:layout_height="30dp"

android:scaleType="centerCrop"

android:id="@+id/iv_icon"/>

<TextView

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:textColor="@android:color/white"

android:textSize="18sp"

android:layout_marginLeft="10dp"

android:id="@+id/tv_title"/>

</LinearLayout>

显示效果图:



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

[java] view
plaincopyprint?

/**

* 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按钮的点击事件,设置左右菜单的开关,原理分析:先设置了缩放方向然后在设置动画,正如我们上面想的一样还设置了动画监听。

[java] view
plaincopyprint?

/**

* 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轴位置。

[java] view
plaincopyprint?

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这个函数如何调用?什么时候调用?请先看看/article/1546778.html

[java] view
plaincopyprint?

@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.菜单关闭方法,同样也设置了动画监听之前的想法也是成立的。

[java] view
plaincopyprint?

/**

* 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.屏蔽菜单方法

[java] view
plaincopyprint?

public void setSwipeDirectionDisable(int direction){

disabledSwipeDirection.add(direction);

}

[java] view
plaincopyprint?

private boolean isInDisableDirection(int direction){

return disabledSwipeDirection.contains(direction);

}

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

[java] view
plaincopyprint?

if (isInIgnoredView || isInDisableDirection(scaleDirection))

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

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



我们来看看它做了什么?

[java] view
plaincopyprint?

@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。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: