SlideMenu: 仿QQ侧滑菜单
SlideMenu
春节的时候在家里完成一个二手交易项目的时候,需求要求做一个侧滑菜单,菜单的形式就跟QQ差不多,效果上跟QQ略有一点差别。QQ在菜单滑动的时候,菜单部分的View两边都有一个隐藏的效果;我们这个App仅仅需要实现最简单的侧滑菜单效果即可,实现复杂程度上,较QQ小了许多。
以前都是项目中有侧滑菜单,我都是拿现成的开源项目SlidingMenu来实现的。放假在家,时间比较充裕,就干脆自己实现了一波。
先上演示图(是用360手机助手实时演示的,所以效果上有些卡顿。在真机上运行很流畅。)
再看看QQ的侧滑菜单演示图
QQ菜单在滑动的时候菜单View两端同时隐藏或滑出
实现思路
对于菜单的横向滑动,我们可以借助Android提供的HorizontalScrollView帮助我们实现。我们先通过继承HorizontalScrollView帮助我们创建一个可以横向滚动的视图。
对于自定义控件,特别是自定义灵活的组合控件,一般要实现一个构造器
public class SlideMenu extends HorizontalScrollView { public SlideMenu(Context context, AttributeSet attrs){ super(context, attrs); } } 第二个参数是一个属性集合,通过它我们能获得一些自定义的属性
在values文件夹下建立一个attrs.xml文件,编写我们需要的自定义属性
在实际项目中,我们一般需要设置菜单View的宽度,菜单以及内容View是使用哪一个布局,所以一般建立以下这三个属性就可以了。有其他需求的也可以自己扩展。
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="SlideMenu"> <attr name="menu_layout" format="reference"></attr> <attr name="content_layout" format="reference"></attr> <attr name="menu_width" format="reference|dimension"></attr> </declare-styleable> </resources>
在SlideMenu类中,我们可以创建一个init()方法,来帮我们完成解析自定义属性的值,并做一些其他初始化参数的操作。
private void init(AttributeSet attrs) { //解析自定义参数的值 TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.SlideMenu); int menuLayoutId = typedArray.getResourceId(0, -1); int contentLayoutId = typedArray.getResourceId(1, -1); mMenuWidth = typedArray.getDimensionPixelOffset(2, 0); //加载View mMenu = View.inflate(getContext(), menuLayoutId, null); mContent = View.inflate(getContext(), contentLayoutId, null); //获取屏幕宽高 DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics(); mScreenWidth = displayMetrics.widthPixels; mScreenHeight = displayMetrics.heightPixels; //设置为不能滑出边界(滑出边界的效果比较难看) setOverScrollMode(OVER_SCROLL_NEVER); //设置无滚动条 setHorizontalScrollBarEnabled(false); }
自定义ViewGroup通常要实现两个方法onMeasure 和 onLayout方法 onMeasure:计算所有ChildView的宽度和高度 然后根据ChildView的计算结果,设置自己的宽和高 onLayout :对子View进行布局,会依次调用子ViewGroup的layout方法
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //设置测量的宽高为屏幕的宽高 setMeasuredDimension( mScreenWidth, mScreenHeight); if(!mHasInit){ //由于会多次测量,所以使用mHasInit保证布局只加载一次 mHasInit = true; //创建子View,并加到ScrollView中 mWrapper = new LinearLayout(getContext()); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(mMenuWidth+mScreenWidth,getMeasuredHeight()); mWrapper.setOrientation(LinearLayout.HORIZONTAL); LinearLayout.LayoutParams menuParams = new LinearLayout.LayoutParams(mMenuWidth, mScreenHeight); mWrapper.addView(mMenu, menuParams); LinearLayout.LayoutParams contentParams = new LinearLayout.LayoutParams(mScreenWidth, mScreenHeight); mWrapper.addView(mContent, contentParams); mWrapper.setLayoutParams(layoutParams); addView(mWrapper); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); //change表示view有新的布局和尺寸。在子view被初始化并加载到父view的时候就会调用onLayout方法,并且change为true if(changed){ //将ScrollView滚动到mMenuWidth的位置,也就是滚动到跟内容View的左侧对其,让菜单栏刚好完全隐藏在屏幕左侧 //注意滚动的时机,当view有新的布局和尺寸时调用,也就是在初始化的时候去滚动。这个时候滚动才不会在打开页面时有滚动的效果 scrollTo(mMenuWidth, 0); } }
这样就基本实现了能够滑动的布局。接下来我们要处理触摸事件,以满足以下要求:
- 当Scroller滚动到0位置时,可以响应任何事件
- 当Scroller滚动到mMenuWidth位置时,只有触摸事件的X坐标小于50dp(大小任意设置)时,才能响应横向滑动事件。
- 当Scroller滚动到0的位置时,点击事件的X坐标大于mMenuWidth时,使菜单View隐藏掉
- 当手指抬起时Scroller滚动到小于mMenuWidth/2位置时,使Scroller平缓滚动到0,当手指抬起时Scroller滚动到大于mMenuWidth/2位置时,使Scroller平缓滚动到mMenuWidth
如果了解了View的事件分发机制,对触摸事件的处理写起来就没有难度,只需要不断的调试就能实现效果,在这里就不贴代码了。
基本使用方法
<com.opensource.slidemenu.SlideMenu android:id="@+id/menu" android:layout_width="match_parent" android:layout_height="match_parent" app:menu_layout="@layout/menu" app:content_layout="@layout/content" app:menu_width="300dp" > </com.opensource.slidemenu.SlideMenu>
在@layout/menu和@layout/content中写布局或者直接引入Fragment都可以,灵活度比较高。
QQ侧滑的思考
虽然没有动手去实现QQ的侧滑,但我也对QQ的侧滑菜单实现方法做了一些思考。
由于QQ菜单在滑动的时候会出现菜单View被压再内容View下面的情况,所以容器肯定不能使用LinearLayout,View叠加的页面我们就需要使用FrameLayout。
侧滑的效果可以这样实现:
我们可以处理触摸事件,在横向滑动的时候,计算滑动距离,根据滑动距离计算此时 菜单View和内容View的距离屏幕最左边的值,然后动态改变菜单View和内容View的margin值。