前一段时间写了一篇 Android小教程 滑动菜单 SlidingMenu 实现方法 (一) 今天拓展一下,实现一个可以实现菜单页联动的SlidingMenu。
其实SlidingMenu的实现方实在太多太多,今天的实现思路在主体上也和上一篇文章中有所不同,尽量和大家分享多一些方法。另外功能上也更丰富一下。
先来解释一下我们要实现的效果:
联动菜单页的意思就是我们在滑动主页面,以显示出或者隐藏菜单页的时候,菜单页相应的也会有一定的移动效果,就好像是主页面带着菜单页面一起滑动一样,
这样在用户体验上比静止不动的菜单页面要更好。除此之外我们额外添加手指快速略过检测速率以及在菜单打开状态下点击主页面关闭菜单的功能。
可能用文字来表述不是很形象,具体的一下效果可以看下面的GIF:
因为我是在虚拟机里面跑的应用然后屏幕截图,总是一卡一卡的,大家先将就看吧,至于点击关闭菜单页以及快速略过打开和关闭菜单页的效果,大家可以在我最下面提供的源码下载里下载代码,然后跑到自己的手机上进行试验。
下面来说我们的实现思路。
我们要实现的这个效果,总体上包含了两部分,菜单页面部分和主页面(美女)部分。我们今天的实现思路是,在初次绘制的过程中,先布局菜单页面,让菜单页面的左上角坐标相对于SlidingMenu坐标系的原点向左侧偏移一定的距离。然后布局主页面,主页面就是整体填充我们的自定义SlidingMenu。
具体的代码如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //设置容器大小,直接填充窗口 int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(width, height); //内部包含两个页面 一个menuPage,一个mainPage //先来设置menuPage的大小,设置为父容器的1/3宽,高度与父容器相同 int menuPageWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() / 3, MeasureSpec.EXACTLY); int menuPageHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY); this.menuPage.measure(menuPageWidthMeasureSpec, menuPageHeightMeasureSpec); //设置mainPage的大小,填充父容器的大小 int mainPageWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY); int mainPageHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY); this.mainPage.measure(mainPageWidthMeasureSpec, mainPageHeightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (!hasMeasured) { //初始化菜单页面左上角坐标,用来第一次布局菜单页面 menuPageLeft = -getMeasuredWidth() / 12; hasMeasured = true; } //校正 if (menuPageLeft > 0) { menuPageLeft = 0; } if (menuPageLeft < -getMeasuredWidth() / 12) { menuPageLeft = -getMeasuredWidth() / 12; } if (mainPageLeft > getMeasuredWidth() / 3) { mainPageLeft = getMeasuredWidth() / 3; } if (mainPageLeft < 0) { mainPageLeft = 0; } int menuPageTop = 0; int mainPageTop = 0; this.menuPage.layout((int)menuPageLeft, menuPageTop, (int) (menuPageLeft + getMeasuredWidth() / 3), menuPageTop + getMeasuredHeight()); this.mainPage.layout((int)mainPageLeft, mainPageTop, (int) (mainPageLeft + getMeasuredWidth()), mainPageTop + getMeasuredHeight()); }
布局完成之后,我们就应该处理触摸过程了,我们的思路是在用户触摸的时候,根据坐标变化,动态的改变menuPage和mainPage的左上角坐标,然后请求重新布局来实现两个页面的滑动效果。具体代码片段如下:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { float x = ev.getX(); if (x < mainPageLeft) { return false; } int action = ev.getAction(); if (action == MotionEvent.ACTION_MOVE && mTouchState != TOUCH_STATE_REST) { return true; } switch (action) { case MotionEvent.ACTION_MOVE: int xDiff = (int) Math.abs(mLastMotionX - x); if (xDiff > mTouchSlop) { //拦截 mTouchState = TOUCH_STATE_SCROLLING; } break; case MotionEvent.ACTION_DOWN: mLastMotionX = x; break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mTouchState = TOUCH_STATE_REST; break; } return mTouchState != TOUCH_STATE_REST; } @Override public boolean onTouchEvent(MotionEvent event) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); float x = event.getX(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: float deltaX = mLastMotionX - x; mainPageLeft -= deltaX; menuPageLeft -= deltaX / 4; requestLayout(); mLastMotionX = x; break; case MotionEvent.ACTION_UP: mVelocityTracker.computeCurrentVelocity(1000); int velocityX = (int) mVelocityTracker.getXVelocity(); if (velocityX > SNAP_VELOCITY) { //显示菜单页面 openMenu(); } else if (velocityX < -SNAP_VELOCITY) { //关闭菜单页面 closeMenu(); } else { snapToDestination(); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } mTouchState = TOUCH_STATE_REST; break; case MotionEvent.ACTION_CANCEL: mTouchState = TOUCH_STATE_REST; break; } return true; }
openMenu()和closeMenu()代码片段如下:这一段需要解释的地方不多:
/** * 快速略过,显示菜单页面 */ public void openMenu() { //主界面 new Thread(new Runnable() { @Override public void run() { while(mainPageLeft <= getMeasuredWidth() / 3) { mainPageLeft += 10; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } uiHandler.obtainMessage(0x00, 0x01, 0x00).sendToTarget(); } } }).start(); //菜单界面 new Thread(new Runnable() { @Override public void run() { while (menuPageLeft <= 0) { menuPageLeft += 2.5; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } uiHandler.obtainMessage(0x00, 0x01, 0x00).sendToTarget(); } } }).start(); } /** * 快速略过,关闭菜单页面 */ public void closeMenu() { //主界面 new Thread(new Runnable() { @Override public void run() { while(mainPageLeft >= 0) { mainPageLeft -= 10; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } uiHandler.obtainMessage(0x00, 0x01, 0x00).sendToTarget(); } } }).start(); //菜单页面 new Thread(new Runnable() { @Override public void run() { while (menuPageLeft >= -getMeasuredWidth() / 12) { menuPageLeft -= 2.5; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } uiHandler.obtainMessage(0x00, 0x01, 0x00).sendToTarget(); } } }).start(); }上面代码中 10 与 2.5 这两个值也是用到了前面 1:4的比例关系。
好了,相信大家仔细读完上面的文字都会明白我实现的思路了,下面是完整的代码和源码下载,我做的注释还是比较全的,仔细看一定能明白原理!
package com.example.demo; import java.lang.ref.WeakReference; import android.content.Context; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.FrameLayout; import android.view.View.OnClickListener; /** * 滑动菜单界面容器 * @author carrey * */ public class SlidingMenuViewGroup extends ViewGroup implements OnClickListener { /** 场景 */ private Context mContext; /** 菜单页面 */ private ViewGroup menuPage; /** 主页面 */ private FrameLayout mainPage; /** 静止状态值 */ private final int TOUCH_STATE_REST = 0; /** 滚动状态值 */ private final int TOUCH_STATE_SCROLLING = 1; /** 当前的滚动与否状态 */ private int mTouchState = TOUCH_STATE_REST; /** 能够拦截左右划屏事件的最小判断距离 */ private int mTouchSlop; /** 记录上一次触摸事件的x坐标 */ private float mLastMotionX; /** 检测快速略过屏幕动作的速率 */ private VelocityTracker mVelocityTracker; /** 快速略过动作有效的最小速度 */ private final int SNAP_VELOCITY = 600; /** 主页面左上角坐标 */ private float mainPageLeft; /** 菜单页面左上角坐标 */ private float menuPageLeft; /** 是否经历过measure过程的标记 */ private boolean hasMeasured; /** 页面Handler */ private UIHandler uiHandler; public SlidingMenuViewGroup(Context context) { super(context); this.mContext = context; init(); } public SlidingMenuViewGroup(Context context, AttributeSet attrs) { super(context, attrs); this.mContext = context; init(); } /** * 一些参数的初始化操作 */ private void init() { mTouchSlop = ViewConfiguration.get(mContext).getScaledEdgeSlop(); uiHandler = new UIHandler(this); } /** * 把菜单页面和主页面添加安装进容器内 * @param menuPage * @param mainPage */ public void setupMenuAndMainPage(ViewGroup menuPage, FrameLayout mainPage) { this.menuPage = menuPage; this.mainPage = mainPage; this.mainPage.setOnClickListener(this); this.addView(this.menuPage); this.addView(this.mainPage); } /** * 点击主界面,在打开的情况下,会关闭菜单页面 */ @Override public void onClick(View v) { if (mainPageLeft == getMeasuredWidth() / 3) { closeMenu(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //设置容器大小,直接填充窗口 int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(width, height); //内部包含两个页面 一个menuPage,一个mainPage //先来设置menuPage的大小,设置为父容器的1/3宽,高度与父容器相同 int menuPageWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() / 3, MeasureSpec.EXACTLY); int menuPageHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY); this.menuPage.measure(menuPageWidthMeasureSpec, menuPageHeightMeasureSpec); //设置mainPage的大小,填充父容器的大小 int mainPageWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY); int mainPageHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY); this.mainPage.measure(mainPageWidthMeasureSpec, mainPageHeightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (!hasMeasured) { //初始化菜单页面左上角坐标,用来第一次布局菜单页面 menuPageLeft = -getMeasuredWidth() / 12; hasMeasured = true; } //校正 if (menuPageLeft > 0) { menuPageLeft = 0; } if (menuPageLeft < -getMeasuredWidth() / 12) { menuPageLeft = -getMeasuredWidth() / 12; } if (mainPageLeft > getMeasuredWidth() / 3) { mainPageLeft = getMeasuredWidth() / 3; } if (mainPageLeft < 0) { mainPageLeft = 0; } int menuPageTop = 0; int mainPageTop = 0; this.menuPage.layout((int)menuPageLeft, menuPageTop, (int) (menuPageLeft + getMeasuredWidth() / 3), menuPageTop + getMeasuredHeight()); this.mainPage.layout((int)mainPageLeft, mainPageTop, (int) (mainPageLeft + getMeasuredWidth()), mainPageTop + getMeasuredHeight()); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { float x = ev.getX(); if (x < mainPageLeft) { return false; } int action = ev.getAction(); if (action == MotionEvent.ACTION_MOVE && mTouchState != TOUCH_STATE_REST) { return true; } switch (action) { case MotionEvent.ACTION_MOVE: int xDiff = (int) Math.abs(mLastMotionX - x); if (xDiff > mTouchSlop) { //拦截 mTouchState = TOUCH_STATE_SCROLLING; } break; case MotionEvent.ACTION_DOWN: mLastMotionX = x; break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mTouchState = TOUCH_STATE_REST; break; } return mTouchState != TOUCH_STATE_REST; } @Override public boolean onTouchEvent(MotionEvent event) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); float x = event.getX(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: float deltaX = mLastMotionX - x; mainPageLeft -= deltaX; menuPageLeft -= deltaX / 4; requestLayout(); mLastMotionX = x; break; case MotionEvent.ACTION_UP: mVelocityTracker.computeCurrentVelocity(1000); int velocityX = (int) mVelocityTracker.getXVelocity(); if (velocityX > SNAP_VELOCITY) { //显示菜单页面 openMenu(); } else if (velocityX < -SNAP_VELOCITY) { //关闭菜单页面 closeMenu(); } else { snapToDestination(); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } mTouchState = TOUCH_STATE_REST; break; case MotionEvent.ACTION_CANCEL: mTouchState = TOUCH_STATE_REST; break; } return true; } /** * UIHandler * @author carrey * */ private static class UIHandler extends Handler { private ViewGroup container; private WeakReference<ViewGroup> containerWR; public UIHandler(ViewGroup container) { this.containerWR = new WeakReference<ViewGroup>(container); this.container = containerWR.get(); } @Override public void handleMessage(Message msg) { switch (msg.arg1) { case 0x01: container.requestLayout(); break; } super.handleMessage(msg); } } /** * 手指缓缓滑动后离开屏幕的处理 */ private void snapToDestination() { //通过mainPageLeft来判断滚动到的页面 if (mainPageLeft >= getMeasuredWidth() / 6) { openMenu(); } else { closeMenu(); } } /** * 快速略过,显示菜单页面 */ public void openMenu() { //主界面 new Thread(new Runnable() { @Override public void run() { while(mainPageLeft <= getMeasuredWidth() / 3) { mainPageLeft += 10; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } uiHandler.obtainMessage(0x00, 0x01, 0x00).sendToTarget(); } } }).start(); //菜单界面 new Thread(new Runnable() { @Override public void run() { while (menuPageLeft <= 0) { menuPageLeft += 2.5; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } uiHandler.obtainMessage(0x00, 0x01, 0x00).sendToTarget(); } } }).start(); } /** * 快速略过,关闭菜单页面 */ public void closeMenu() { //主界面 new Thread(new Runnable() { @Override public void run() { while(mainPageLeft >= 0) { mainPageLeft -= 10; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } uiHandler.obtainMessage(0x00, 0x01, 0x00).sendToTarget(); } } }).start(); //菜单页面 new Thread(new Runnable() { @Override public void run() { while (menuPageLeft >= -getMeasuredWidth() / 12) { menuPageLeft -= 2.5; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } uiHandler.obtainMessage(0x00, 0x01, 0x00).sendToTarget(); } } }).start(); } }