手机QQ侧滑菜单_从源码上一步步解析效果的实现

本文思想来自洪洋大哥,本来写的原创的,有些朋友看到标题后认为是照搬翔哥的例子,仔细看看,会有不同,不过其中的主要思想还是翔哥的,滑动方面的算法还真是有些区别的,看完了就知道不一样,而且我这人比较啰嗦,细节介绍的也比较过多,一些可能遇到的问题也一一给列出了。

本文出自吕中宜的博客,转载请标明出处:手机QQ侧滑菜单_从源码上一步步解析效果的实现

看到网上有很多关于侧滑菜单的例子与讲解,今天我也带来一个手机QQ侧滑菜单的例子,不多说,先上项目视图和效果。

讲解前的话:我们写软件,要有一个明确的目标,明确你要干什么,要有一个详细的计划与对项目的分析,接下来我所写的重点不在于代码,在于教大家去分析问题,分解问题,凡事预则立,不预则废.

  首先我们要写这么一个特殊的侧滑菜单,作一个初步的分析,大致分为三步。第一步:分析控件,实现布局。第二步:实现普通侧滑菜单。第三步:改造普通侧滑菜单为手机QQ侧滑菜单。

第一步:分析控件与实现布局;

  既然我们要实现侧滑菜单,就要先分析滑动菜单是什么,其实,不管是什么滑动菜单,左滑的,右滑的,上滑的,下滑的,按键的,他们在我们程序员眼里就应该是一样的,我们要透过现象看本质,侧滑菜单的本质就是两个布局的替换,一个Content(内容)布局,一个Menu(菜单)布局,所以在这里,我们确定了我们侧滑菜单的主角,Content布局和Menu布局,这两个布局我们自己实现,代码如下。

Menu布局,随便写点什么,如果你很懒的话,可以空着都没问题,只需要知道,你打开菜单后,显示的就是在这个布局中的View

<?xml version="1.0" encoding="utf-8"?>
	<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	    android:layout_width="wrap_content"
	    android:layout_height="wrap_content"
	    android:orientation="vertical" 
	    android:gravity="center|left">
	    <TextView
	        android:layout_width="wrap_content"
	        android:layout_height="wrap_content"
	        android:text="这个是侧滑菜单" />
	    <TextView
	        android:layout_width="wrap_content"
	        android:layout_height="wrap_content"
	        android:text="这个是侧滑菜单" />
	    <TextView
	        android:layout_width="wrap_content"
	        android:layout_height="wrap_content"
	        android:text="这个是侧滑菜单" />
	
</LinearLayout>

Content布局,跟Menu布局同理,这里不在做多解释,只要会生成Android工程就会写。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<span style="white-space:pre">	</span>android:layout_width="match_parent"
<span style="white-space:pre">	</span>android:layout_height="match_parent"
<span style="white-space:pre">	</span>android:orientation="vertical" 
<span style="white-space:pre">	</span>android:background="@android:color/holo_blue_dark">
</LinearLayout>

写到这里,如果单纯的将这两个布局拼到一起,给大家看一个假象的效果,就是这样:


这样,我们的Menu在手机的屏幕中央,只是单纯的做出了两个布局,我们怎样让menucontent可以在我们的手机屏幕(screen)中切换呢

    接下来,我们就要想用什么来替换着两个布局呢,其实我们开始就应该想到了,我们却少一个载体,这里我就比作是一辆车,一辆可以把Menu和Content布局拖到我们屏幕中间的车,这辆车在代码中就是一个控件,什么控件有这种功能呢,ScrollView,HorizontalScrollView,还有一些控件都带这个功能,我们要做的是侧滑,就选择HorizontalScrollView控件,将两个布局并排放到这个控件中,类似于两个固定的物品被装上了轮子,就像这样(实际上不是这样的,实际中是挤在一起的,实际上还要在java代码中给两个布局设置宽度都等于屏幕宽度,才会出现下图效果,因为后面我们会讲到在代码中设置布局宽度,所以这里不讲,你们看了后面在到这一步设置会看到效果):

 

	<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
	    android:layout_width="match_parent"
	    android:layout_height="match_parent" >
	    <LinearLayout
	        android:layout_width="match_parent"
	        android:layout_height="match_parent"
	        android:orientation="horizontal" >
	        <!-- 这个是Menu的布局  如果把layout_width = “你们的手机屏幕宽度”,也是可以看到效果的-->
	        <LinearLayout
	            android:layout_width="match_parent"
	            android:layout_height="match_parent" >
	            <include layout="@layout/menu" />
	        </LinearLayout>
	        <!-- 这个是Content的布局 -->
	        <LinearLayout
	            android:layout_width="match_parent"
	            android:layout_height="match_parent" >
	            <include  layout="@layout/content"/>
	        </LinearLayout>
	    </LinearLayout>
	</HorizontalScrollView>

这样,我们就能在屏幕上面滑动我们的手指来控制MenuConten显示在我们的屏幕上,第一步就ok了。


第二步:实现普通侧滑菜单

上面的代码中,我们实现了左右滑动,但是这种滑动是没有约束的,我们甚至可以滑动显示Menu的右边和Content的左边显示在屏幕上,如图:


 
 

这样的效果还没有达到我们侧滑的预期,实际上,我们平时用到的控件,手指滑动到如上图所示这样,屏幕会自动滑动到显示Menu或者显示Content,不会显示一边一半的,要实现自动矫正,就要能控制HorizontalScroollView,而我们使用的HorizontalScrollView是不受控的,不受控这句话说的也不严谨,应该说不能完全受控,我们要让HorizontalScrollView完全在我们的掌握之中,就要重写HorizontalScrollView控件,就是用自定义控件,一说自定义控件,新手就有点发慌了,其实,自定义控件也没想象中的那么难,这就好比,动物园需要训练一只老虎去表演杂技,野生的老虎,驯化起来,很难,可以说是基本不可能,但是我们可以驯化一只野老虎的崽,这样就容易得多,而且,它与野生虎的特性基本一致,但是可控性就高了很多,这就是我对自定义控件的理解,(要知道,程序设计来源于生活),好了,我们开始写一个自己的类来继承HorizontalScrollView,控制它去帮我们实现侧滑菜单,先贴出代码,一步步详细解释:

public class MyHorizontalScrollView extends HorizontalScrollView {
	
		public MyHorizontalScrollView(Context context, AttributeSet attrs) {
			super(context, attrs);
			
		}
	
}


      继承HorizontalScrollView类必须重写HorizontalScrollView中的任意一个构造方法(需要注意的是,重写控件之后,我们布局文件中的<HorizontalScrollView>标签要换成我们自定义控件,也就是包名+类名,我这里是<com.lv.qq5.MyHorizontalScrollView>),HorizontalScrollView中有四个构造方法(旧版只有三个的),第一个是没有设置控件属性时候调用,第二个是设置了布局属性调用,第三个是设置了style时候调用,第四的是引用了资源时候调用。因为我们的HorizontalScrollView中设置过android:layout_width="match_parent"android:layout_height="match_parent",我们这里重写第二个构造方法。在这个构造方法中,我们得到手机屏幕的宽和高

<span style="white-space:pre">		</span>private int ScreenWidth;//屏幕宽度
		private int ScreenHeight;//屏幕高度
		public MyHorizontalScrollView(Context context,AttributeSet attrs) {
			super(context,attrs);
			WindowManager wm = (WindowManager) context
					.getSystemService(Context.WINDOW_SERVICE);
			DisplayMetrics outMetrics = new DisplayMetrics();
			wm.getDefaultDisplay().getMetrics(outMetrics);
			ScreenWidth = outMetrics.widthPixels;
			ScreenHeight = outMetrics.heightPixels;
		}

WindowManager对象是android中的一个重要服务,它可以将用户的操作翻译成指令,这里不做过多解释,把他当成一个传话的工具就行了,比如我们触摸了屏幕一下,它就会告诉android系统,屏幕被触摸了。通过WindowManager中的一个方法可以得到当前屏幕的信息保存到DisplayMetrics对象中,再从DisplayMetrics对象的方法中可以得到屏幕的宽高。还有一种方式可以得到屏幕宽高wm.getDefaultDisplay().getWidth()wm.getDefaultDisplay().getWidth();;,不过已经被google标注为过时了,我们就以前面的方法为准。我们将得到的屏幕宽高保存在全局变量中,后面会用到。接下来,我们重写HorizontalScrollViewonMeasure方法和onLayout,这两个方法在自定义控件中经常会用到,onMeasure方法是测量方法,onLayout方法是布局方法,一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw()三个步骤,这个基础要自己补习一下,推荐一下郭霖博士的博客

http://blog.csdn.net/guolin_blog/article/details/16330267,写的比较详细,对view的绘制讲解的很独到,不过,我们这里只需要知道绘制流程流程中的这两步就可以搞定我们的需求了,接下来,回到我们的主题,重写onMeasure()方法:

<span style="white-space:pre">	</span>private ViewGroup linearLayout;//HorizontalScrollView中的直接子控件
	private ViewGroup mMenu;	//Menu布局控件
	private ViewGroup mContent;//content布局控件
	private boolean b = true;
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		if (b) {
			linearLayout = (ViewGroup) this.getChildAt(0);
			mMenu = (ViewGroup) linearLayout.getChildAt(0);
			mContent = (ViewGroup) linearLayout.getChildAt(1);

			mMenu.getLayoutParams().width = mContent.getLayoutParams().width = ScreenWidth;
			b = false;
		}
	}

这里的linearLayout,对应的就是布局文件中的HorizontalScrollView的直接子控件的那个LinearLayout,我在这里取这个名字也是一种暗示,对应的控间,便于理解,你们可以边写边对照着布局看, this.getChildAt(0);this代表的就是当前的MyHorizontalScrollView.getChildAt(0)就是得到他的第一个子布局, linearLayout.getChildAt(0);就是MyHorizontalScrollView的第一个直接子布局的第一个直接子布局,说的有点饶舌,不过,理就是这个理,仔细揣摩,逻辑很顺的,这里反复得到子布局就是为了弄到mMenumContent实例,然后getLayoutParams得到他们的布局参数中的宽度和高度,设置为屏幕的宽和高,外面套一个只执行一次的条件,让这些设置被封成一次性代码块(呵呵,我取的名字,因为找不到好的专业性名词来形容),也就是让设置控件尺寸的代码只执行一次,多执行只会无故消耗性能,这也算是代码优化一个小地方。这个方法被执行后,我们的布局上的控件加起来就能保证刚好是两个手机屏幕大小。

接下来重写onLayout()方法,也就是布局方法,代码很少,就一句,设置HorizontalScrollView滚动到Content的位置,让Content显示在屏幕中,menu移动到屏幕外,效果和代码贴上:


<span style="white-space:pre">		</span>protected void onLayout(boolean changed, int l, int t, int r, int b) {
			super.onLayout(changed, l, t, r, b);
			if(changed){
				this.scrollTo(mMenu.getLayoutParams().width, 0);
			}
		}

changed参数是得到屏幕当前的状态是否被改变,改变=true,没改变=false,我们只需要知道这个参数就行了,布局被加载到屏幕的时候,屏幕的状态是被改变的,执行scrollTo,值得注意的是,有时候因为view加载步骤的问题,scrollTo方法或者smoothScrollTo方法不会生效,(这个解释起来就比较多了,又要讲到源码里面去了,我就不把问题弄复杂了,谁想听的我再出一篇详细解释)我们就要换成这种写法:

<span style="white-space:pre">	</span>MyHorizontalScrollView.this.post(new Runnable() {
				public void run() {
					MyHorizontalScrollView.this.scrollTo(
							mMenu.getLayoutParams().width, 0);
				}
			});


这个post方法就是指当MyHorizontalScrollView加载完成时,我们这里套这个post方法了,就不用外面加if判断条件了,scrollTo就是让HorizongtalScrollView的直接子类滚动到相应的位置,这里滚动的x轴是与HorizontalScrollView左边的距离,刚好是我们Menu的宽度,执行效果就会如上图所示。接下来,我们要实现手指滑动操作后的图形自动校正功能,就要重写

onTouchEvent方法,手指触摸事件,分析一下,我们要监控的是手指松开的事件,其它的不管,手指松开后,让view呈现怎样的效果呢,手机上的软件大多数都是这么个标准,如果滑动了屏幕的一半,就认为用户的目的是让屏幕变化,如果滚动没有超过一半,则认为是用户误触(当然,这个标准你可以自己设定,我们这里以一半为标准),好了思路理清楚了,我们直接上代码:

<span style="white-space:pre">	</span>public boolean onTouchEvent(MotionEvent ev) {
			if(ev.getAction() ==MotionEvent.ACTION_UP){
				int scrollX = getScrollX();
				if(scrollX>mMenu.getLayoutParams().width/2){
					this.smoothScrollTo(mMenu.getLayoutParams().width, 0);
				}else{
					this.smoothScrollTo(0, 0);
				}
				return true;
			}
			return super.onTouchEvent(ev);
		}
<span style="white-space:pre">	</span>

if(ev.getAction() ==MotionEvent.ACTION_UP)就是判断手指离开屏幕的时候,

int scrollX = getScrollX();是得到当前的偏移量,也就是屏幕的左边与HorizontalScrollView左边的距离,如果这个距离大于mMenu.getLayoutParams().width/2(大于menu宽度的一半),我们就让

this.smoothScrollTo(mMenu.getLayoutParams().width,0);,让HorizongtalScrollView滚动到显示content,如果没有,就滚动到最左边(这里必须要returen true,因为HorizontalScrollViewViewGroup的间接之类,ViewGrouponTouchEvent事件返回的super.onTouchEvent(ev)是为false,这样就能吧Touch事件让给ViewGroup中的view,这里我们的HorizontalScrollView要想占有Touch事件,所以就return true,解释到这里,我也是醉了,自己捣鼓了那么久的各种原理都倾其奉上了,看过的各位美女帅哥留个好评呗),也就是显示menu,完成后,一个普通的侧滑菜单就完成了。




三、改造普通侧滑菜单为手机QQ侧滑菜单

首先,我们依旧是分析问题,到这一步了,我们分析的主要问题就是QQ侧滑与普通侧滑的区别,QQ侧滑与普通侧滑有三点不同点,1menu不是在content左边,而是在后面出现,业界称之为抽屉式侧滑2content滑动的时候会缩小(这个缩放目测在10.7之间)但是不会离开屏幕(保留一部分的内容)3menu中的内容是动画显示的(其中有一个透明度0.31的变化,缩放了0.71的变化),好了,分析完成,我们再从第一个开始实施,就是要在滑动的时候,让menu一直处于屏幕之中,准确来说应该是menu的布局位置处于屏幕中,但是也许被遮挡(这点很好办,我们可以取巧,技巧下面会概述),这3个点都必须要一个类配合,这个类是onScrollChanged,这个方法是在HorizontalScrollView发生改变的时候调用,准确来说是滑动的时候调用,onScrollChanged方法有四个参数int l, int t, int oldl, int oldt,分别代表偏移后的lefttop和偏移前的lefttop,我们这里只需要用到l,离左边的距离,我们让HorizontalScrollView滑动的时候,动态的设置Menu的位置,就可以实现我们要的效果了,怎么动态设置Menu的位置呢?l的用处就到了,其实写了这么多原理与分析,实际上就一句代码,这里我再次强调一下原理很重要,知其然要知其所有然,半知不解可不是什么好事情。附上代码:

<span style="white-space:pre">	</span>protected void onScrollChanged(int l, int t, int oldl, int oldt) {
			super.onScrollChanged(l, t, oldl, oldt);
			mMenu.setTranslationX(l);
		}
<span style="white-space:pre">	</span>

mMenu.setTranslationX(l);就是让menu视图移动到HorizontalScrollView的偏移量这里,HorizontalScrollView的偏移量就是显示在屏幕的位置,至于刚刚说取巧的地方就在这里体现了,一般在布局文件中,如果前一个View与后一个View的位置重合,显示在上面的就是后一个View,也就是我们的Content,刚好setTranslationX内的参数利用用了HorizontalScrollView的偏移量,content覆盖menu利用了android的绘制原理,浓缩成一行代码,一句定精髓(这里容我吹一下牛,网上那些知名博客里面的也有这个过程,比我这个复杂的多,他们应该是没有时间来揣摩这么小的细节的),好了,ok,第1个小点被搞定了。

接着来分析第二小点:content滑动的时候会缩小(这个缩放目测在10.7之间)但是不会离开屏幕(保留一部分的内容),实际上我们还是要用到onScrollChanged这个方法,只不过接下来的就不是让控件移动了,而是让控件缩放,(补充一下,都要用到属性动画,刚刚的

mMenu.setTranslationX(l);就是属性动画的一种,属性动画是android3.1以后出来的,所以我们的项目要设置为3.1以上3.1一下的要用到属性动画就要去GitHub上下载属性动画的开源jar包),言归正传,我们要让content的缩放从1变化到0.7,这个值怎么计算,我们依旧用到ll真是一个神器,我们要做动画,就经常要用到0-1之间的一个值,这里一个技巧,l最大的时候是mMenu的宽度,最小的时候是0,用l去除以mMenu的宽度,刚好就可以得到0-1之间的值,那怎样得到0.71之间的值呢,:很简单,用0.7+0.3×(l除以mMenu的宽度),老规矩,上代码,onScrollChanged中加入:



 

<span style="white-space:pre">	</span>mContent.setScaleX((float)(l*1.0f/mMenu.getLayoutParams().width*0.3+0.7));
<span style="white-space:pre">	</span>mContent.setScaleY((float)(l*1.0f/mMenu.getLayoutParams().height*0.3+0.7));<span style="white-space:pre">	</span>


乘以0.1是为了避免在运算中将小数点后面丢失掉了,好了,滑动缩小ok,接着来content要留一小部分在屏幕上,不能完全消失,缩放动画前可以设置缩放中心,我们控制这个缩放中心就ok了,在setScaleXsetScaleY前面加上xy的缩放中心,先上代码再解释:

<span style="white-space:pre">	</span>mContent.setPivotX(-1*(ScreenWidth));
	mContent.setPivotY(ScreenHeight/2);
<span style="white-space:pre">	</span>


先易后难的原则,Y轴的缩放中心设置为屏幕的一半,没有需要解释吧,主要是x轴的缩放中心,等于负的屏幕的宽度,就是把缩放点设置在menu上,如果不设置,缩放点就在content上了,我们显示menu的时候,content的缩放点如果在content上,就在屏幕外面了,ok,我们的缩放不并留内容就ok了,进入最后一步,menu内容的缩放与透明度变化,仔细看QQmenu菜单出来的时候,实际上就是缩放变大并由半透明到不透明的变化,分析出来了就简单了,依葫芦画瓢,我们依旧利用偏移量l来做文章,呵呵,不管是不是老一套,管用就行,androidui,玩的就是一个技巧,在之前获取控件并设置尺寸的代码中,多加一个得到menu视图的子视图的代码保存到全局变量mMenuChild
mMenuChild= (ViewGroup) mMenu.getChildAt(0);
 
再到onScrollChanged后面加上:
<span style="white-space:pre">	</span>mMenuChild.setPivotX(-30);
	mMenuChild.setPivotY(ScreenHeight/2);
	mMenuChild.setScaleX((float) (0.3-1.0*l/ScreenWidth*0.3+0.7));
	mMenuChild.setScaleY((float) (0.3-1.0*l/ScreenWidth*0.3+0.7));
<span style="white-space:pre">	</span><pre name="code" class="java"><span style="white-space:pre">	</span>mMenuChild.setAlpha(0.5f-1.0f*l/(float)mMenu.getLayoutParams().width*0.5f+0.5f);
 
<span style="white-space:pre">	</span>
因为content是缩小,我们的mMenuChild是放大,所以我们用0.3去减去算出的0-0.3之间的值,透明度同理0.5-1
看到这里,是不是被这些值弄的稀里糊涂的了,我来用图形给解析一下



如图所示,l的值就是HorizongtalScrollView的最左边与屏幕左边的距离,erscreenWidth是屏幕的宽度,同时也是Menu和Content的宽度,HorizongtalScrollView在滚动中的适合,l的值动态变化,l值最小的时候是0,最大的时候刚好就是menu的宽度,也就是屏幕的宽度,我们用l去除以屏幕的宽度,刚好就可以得到0-1之间的一个数,0-1之间的数是做动画最好的值了,用这个值我们可以调出任何自己想要的效果,比如我们例子中的0.7-1的值,可以这么调,1.0*l/ScreenWidth*0.3得到的就是一个0-0.3之间的值,再加上0.7,刚好就是0.7-1的值了。

好了,一个完美的手机QQ的侧滑菜单就此结束,作者刚开始写博客,页面弄的有点乱,大家包涵下。


在此感谢csdn的高手郭霖http://my.csdn.net/sinyu890807和鸿洋http://my.csdn.net/lmj623565791

附上源码http://download.csdn.net/detail/u012296101/8184811,我先前说过,我讲的主要是一种思维设计模式,所以源码跟我所讲是有偏差的,因为我自己每次写这个特效都不一定用的同一种计算方式,所以还是先弄懂了再思考源码,自己揣摩

基于有的朋友找我要右侧的菜单栏,我大概改了一下,顺便也上传了http://download.csdn.net/detail/u012296101/8191657


  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值