`
从此醉
  • 浏览: 1046327 次
  • 性别: Icon_minigender_1
  • 来自: US
社区版块
存档分类
最新评论

Android滑动菜单框架完全解析,教你如何一分钟实现滑动菜单特效

 
阅读更多

之前我向大家介绍了史上最简单的滑动菜单的实现方式,相信大家都还记得。如果忘记了其中的实现原理或者还没看过的朋友,请先去看一遍之前的文章Android滑动菜单特效实现,仿人人客户端侧滑效果,史上最简单的侧滑实现,因为我们今天要实现的滑动菜单框架也是基于同样的原理的。

之前的文章中在最后也提到了,如果是你的应用程序中有很多个Activity都需要加入滑动菜单的功能,那么每个Activity都要写上百行的代码才能实现效果,再简单的滑动菜单实现方案也没用。因此我们今天要实现一个滑动菜单的框架,然后在任何Activity中都可以一分钟引入滑动菜单功能。

首先还是讲一下实现原理。说是滑动菜单的框架,其实说白了也很简单,就是我们自定义一个布局,在这个自定义布局中实现好滑动菜单的功能,然后只要在Activity的布局文件里面引入我们自定义的布局,这个Activity就拥有了滑动菜单的功能了。原理讲完了,是不是很简单?下面我们来动手实现吧。

在Eclipse中新建一个Android项目,项目名就叫做RenRenSlidingLayout。

新建一个类,名叫SlidingLayout,这个类是继承自LinearLayout的,并且实现了OnTouchListener接口,具体代码如下:

  1. publicclassSlidingLayoutextendsLinearLayoutimplementsOnTouchListener{
  2. /**
  3. *滚动显示和隐藏左侧布局时,手指滑动需要达到的速度。
  4. */
  5. publicstaticfinalintSNAP_VELOCITY=200;
  6. /**
  7. *屏幕宽度值。
  8. */
  9. privateintscreenWidth;
  10. /**
  11. *左侧布局最多可以滑动到的左边缘。值由左侧布局的宽度来定,marginLeft到达此值之后,不能再减少。
  12. */
  13. privateintleftEdge;
  14. /**
  15. *左侧布局最多可以滑动到的右边缘。值恒为0,即marginLeft到达0之后,不能增加。
  16. */
  17. privateintrightEdge=0;
  18. /**
  19. *左侧布局完全显示时,留给右侧布局的宽度值。
  20. */
  21. privateintleftLayoutPadding=80;
  22. /**
  23. *记录手指按下时的横坐标。
  24. */
  25. privatefloatxDown;
  26. /**
  27. *记录手指移动时的横坐标。
  28. */
  29. privatefloatxMove;
  30. /**
  31. *记录手机抬起时的横坐标。
  32. */
  33. privatefloatxUp;
  34. /**
  35. *左侧布局当前是显示还是隐藏。只有完全显示或隐藏时才会更改此值,滑动过程中此值无效。
  36. */
  37. privatebooleanisLeftLayoutVisible;
  38. /**
  39. *左侧布局对象。
  40. */
  41. privateViewleftLayout;
  42. /**
  43. *右侧布局对象。
  44. */
  45. privateViewrightLayout;
  46. /**
  47. *用于监听侧滑事件的View。
  48. */
  49. privateViewmBindView;
  50. /**
  51. *左侧布局的参数,通过此参数来重新确定左侧布局的宽度,以及更改leftMargin的值。
  52. */
  53. privateMarginLayoutParamsleftLayoutParams;
  54. /**
  55. *右侧布局的参数,通过此参数来重新确定右侧布局的宽度。
  56. */
  57. privateMarginLayoutParamsrightLayoutParams;
  58. /**
  59. *用于计算手指滑动的速度。
  60. */
  61. privateVelocityTrackermVelocityTracker;
  62. /**
  63. *重写SlidingLayout的构造函数,其中获取了屏幕的宽度。
  64. *
  65. *@paramcontext
  66. *@paramattrs
  67. */
  68. publicSlidingLayout(Contextcontext,AttributeSetattrs){
  69. super(context,attrs);
  70. WindowManagerwm=(WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
  71. screenWidth=wm.getDefaultDisplay().getWidth();
  72. }
  73. /**
  74. *绑定监听侧滑事件的View,即在绑定的View进行滑动才可以显示和隐藏左侧布局。
  75. *
  76. *@parambindView
  77. *需要绑定的View对象。
  78. */
  79. publicvoidsetScrollEvent(ViewbindView){
  80. mBindView=bindView;
  81. mBindView.setOnTouchListener(this);
  82. }
  83. /**
  84. *将屏幕滚动到左侧布局界面,滚动速度设定为30.
  85. */
  86. publicvoidscrollToLeftLayout(){
  87. newScrollTask().execute(30);
  88. }
  89. /**
  90. *将屏幕滚动到右侧布局界面,滚动速度设定为-30.
  91. */
  92. publicvoidscrollToRightLayout(){
  93. newScrollTask().execute(-30);
  94. }
  95. /**
  96. *左侧布局是否完全显示出来,或完全隐藏,滑动过程中此值无效。
  97. *
  98. *@return左侧布局完全显示返回true,完全隐藏返回false。
  99. */
  100. publicbooleanisLeftLayoutVisible(){
  101. returnisLeftLayoutVisible;
  102. }
  103. /**
  104. *在onLayout中重新设定左侧布局和右侧布局的参数。
  105. */
  106. @Override
  107. protectedvoidonLayout(booleanchanged,intl,intt,intr,intb){
  108. super.onLayout(changed,l,t,r,b);
  109. if(changed){
  110. //获取左侧布局对象
  111. leftLayout=getChildAt(0);
  112. leftLayoutParams=(MarginLayoutParams)leftLayout.getLayoutParams();
  113. //重置左侧布局对象的宽度为屏幕宽度减去leftLayoutPadding
  114. leftLayoutParams.width=screenWidth-leftLayoutPadding;
  115. //设置最左边距为负的左侧布局的宽度
  116. leftEdge=-leftLayoutParams.width;
  117. leftLayoutParams.leftMargin=leftEdge;
  118. leftLayout.setLayoutParams(leftLayoutParams);
  119. //获取右侧布局对象
  120. rightLayout=getChildAt(1);
  121. rightLayoutParams=(MarginLayoutParams)rightLayout.getLayoutParams();
  122. rightLayoutParams.width=screenWidth;
  123. rightLayout.setLayoutParams(rightLayoutParams);
  124. }
  125. }
  126. @Override
  127. publicbooleanonTouch(Viewv,MotionEventevent){
  128. createVelocityTracker(event);
  129. switch(event.getAction()){
  130. caseMotionEvent.ACTION_DOWN:
  131. //手指按下时,记录按下时的横坐标
  132. xDown=event.getRawX();
  133. break;
  134. caseMotionEvent.ACTION_MOVE:
  135. //手指移动时,对比按下时的横坐标,计算出移动的距离,来调整左侧布局的leftMargin值,从而显示和隐藏左侧布局
  136. xMove=event.getRawX();
  137. intdistanceX=(int)(xMove-xDown);
  138. if(isLeftLayoutVisible){
  139. leftLayoutParams.leftMargin=distanceX;
  140. }else{
  141. leftLayoutParams.leftMargin=leftEdge+distanceX;
  142. }
  143. if(leftLayoutParams.leftMargin<leftEdge){
  144. leftLayoutParams.leftMargin=leftEdge;
  145. }elseif(leftLayoutParams.leftMargin>rightEdge){
  146. leftLayoutParams.leftMargin=rightEdge;
  147. }
  148. leftLayout.setLayoutParams(leftLayoutParams);
  149. break;
  150. caseMotionEvent.ACTION_UP:
  151. //手指抬起时,进行判断当前手势的意图,从而决定是滚动到左侧布局,还是滚动到右侧布局
  152. xUp=event.getRawX();
  153. if(wantToShowLeftLayout()){
  154. if(shouldScrollToLeftLayout()){
  155. scrollToLeftLayout();
  156. }else{
  157. scrollToRightLayout();
  158. }
  159. }elseif(wantToShowRightLayout()){
  160. if(shouldScrollToContent()){
  161. scrollToRightLayout();
  162. }else{
  163. scrollToLeftLayout();
  164. }
  165. }
  166. recycleVelocityTracker();
  167. break;
  168. }
  169. returnisBindBasicLayout();
  170. }
  171. /**
  172. *判断当前手势的意图是不是想显示右侧布局。如果手指移动的距离是负数,且当前左侧布局是可见的,则认为当前手势是想要显示右侧布局。
  173. *
  174. *@return当前手势想显示右侧布局返回true,否则返回false。
  175. */
  176. privatebooleanwantToShowRightLayout(){
  177. returnxUp-xDown<0&&isLeftLayoutVisible;
  178. }
  179. /**
  180. *判断当前手势的意图是不是想显示左侧布局。如果手指移动的距离是正数,且当前左侧布局是不可见的,则认为当前手势是想要显示左侧布局。
  181. *
  182. *@return当前手势想显示左侧布局返回true,否则返回false。
  183. */
  184. privatebooleanwantToShowLeftLayout(){
  185. returnxUp-xDown>0&&!isLeftLayoutVisible;
  186. }
  187. /**
  188. *判断是否应该滚动将左侧布局展示出来。如果手指移动距离大于屏幕的1/2,或者手指移动速度大于SNAP_VELOCITY,
  189. *就认为应该滚动将左侧布局展示出来。
  190. *
  191. *@return如果应该滚动将左侧布局展示出来返回true,否则返回false。
  192. */
  193. privatebooleanshouldScrollToLeftLayout(){
  194. returnxUp-xDown>screenWidth/2||getScrollVelocity()>SNAP_VELOCITY;
  195. }
  196. /**
  197. *判断是否应该滚动将右侧布局展示出来。如果手指移动距离加上leftLayoutPadding大于屏幕的1/2,
  198. *或者手指移动速度大于SNAP_VELOCITY,就认为应该滚动将右侧布局展示出来。
  199. *
  200. *@return如果应该滚动将右侧布局展示出来返回true,否则返回false。
  201. */
  202. privatebooleanshouldScrollToContent(){
  203. returnxDown-xUp+leftLayoutPadding>screenWidth/2
  204. ||getScrollVelocity()>SNAP_VELOCITY;
  205. }
  206. /**
  207. *判断绑定滑动事件的View是不是一个基础layout,不支持自定义layout,只支持四种基本layout,
  208. *AbsoluteLayout已被弃用。
  209. *
  210. *@return如果绑定滑动事件的View是LinearLayout,RelativeLayout,FrameLayout,
  211. *TableLayout之一就返回true,否则返回false。
  212. */
  213. privatebooleanisBindBasicLayout(){
  214. if(mBindView==null){
  215. returnfalse;
  216. }
  217. StringviewName=mBindView.getClass().getName();
  218. returnviewName.equals(LinearLayout.class.getName())
  219. ||viewName.equals(RelativeLayout.class.getName())
  220. ||viewName.equals(FrameLayout.class.getName())
  221. ||viewName.equals(TableLayout.class.getName());
  222. }
  223. /**
  224. *创建VelocityTracker对象,并将触摸事件加入到VelocityTracker当中。
  225. *
  226. *@paramevent
  227. *右侧布局监听控件的滑动事件
  228. */
  229. privatevoidcreateVelocityTracker(MotionEventevent){
  230. if(mVelocityTracker==null){
  231. mVelocityTracker=VelocityTracker.obtain();
  232. }
  233. mVelocityTracker.addMovement(event);
  234. }
  235. /**
  236. *获取手指在右侧布局的监听View上的滑动速度。
  237. *
  238. *@return滑动速度,以每秒钟移动了多少像素值为单位。
  239. */
  240. privateintgetScrollVelocity(){
  241. mVelocityTracker.computeCurrentVelocity(1000);
  242. intvelocity=(int)mVelocityTracker.getXVelocity();
  243. returnMath.abs(velocity);
  244. }
  245. /**
  246. *回收VelocityTracker对象。
  247. */
  248. privatevoidrecycleVelocityTracker(){
  249. mVelocityTracker.recycle();
  250. mVelocityTracker=null;
  251. }
  252. classScrollTaskextendsAsyncTask<Integer,Integer,Integer>{
  253. @Override
  254. protectedIntegerdoInBackground(Integer...speed){
  255. intleftMargin=leftLayoutParams.leftMargin;
  256. //根据传入的速度来滚动界面,当滚动到达左边界或右边界时,跳出循环。
  257. while(true){
  258. leftMargin=leftMargin+speed[0];
  259. if(leftMargin>rightEdge){
  260. leftMargin=rightEdge;
  261. break;
  262. }
  263. if(leftMargin<leftEdge){
  264. leftMargin=leftEdge;
  265. break;
  266. }
  267. publishProgress(leftMargin);
  268. //为了要有滚动效果产生,每次循环使线程睡眠20毫秒,这样肉眼才能够看到滚动动画。
  269. sleep(20);
  270. }
  271. if(speed[0]>0){
  272. isLeftLayoutVisible=true;
  273. }else{
  274. isLeftLayoutVisible=false;
  275. }
  276. returnleftMargin;
  277. }
  278. @Override
  279. protectedvoidonProgressUpdate(Integer...leftMargin){
  280. leftLayoutParams.leftMargin=leftMargin[0];
  281. leftLayout.setLayoutParams(leftLayoutParams);
  282. }
  283. @Override
  284. protectedvoidonPostExecute(IntegerleftMargin){
  285. leftLayoutParams.leftMargin=leftMargin;
  286. leftLayout.setLayoutParams(leftLayoutParams);
  287. }
  288. }
  289. /**
  290. *使当前线程睡眠指定的毫秒数。
  291. *
  292. *@parammillis
  293. *指定当前线程睡眠多久,以毫秒为单位
  294. */
  295. privatevoidsleep(longmillis){
  296. try{
  297. Thread.sleep(millis);
  298. }catch(InterruptedExceptione){
  299. e.printStackTrace();
  300. }
  301. }
  302. }
看到这里,我相信大家一定会觉得这些代码非常熟悉。没错,基本上这些代码和之前那篇文章的代码大同小异,只不过以前这些代码是写在Activity里的,而现在我们移动到了自定义的View当中。

接着我来说明一下和以前不同的部分。我们可以看到,这里将onLayout方法进行了重写,使用getChildAt(0)获取到的布局作为左边布局,使用getChildAt(1)获取到的布局作为右边布局。并将左边布局的宽度重定义为屏幕宽度减去leftLayoutPadding,将右侧布局的宽度重定义为屏幕宽度。然后让左边布局偏移出屏幕,这样能看到的就只有右边布局了。因此在这里我们也可以看出,使用SlidingLayout这个布局的前提条件,必须为这个布局提供两个子元素,第一个元素会作为左边布局偏移出屏幕,第二个元素会作为右边布局显示在屏幕上。

然后我们看一下setScrollEvent方法,这个方法接收一个View作为参数,然后为这个View绑定了一个touch事件。这是什么意思呢?让我们来想象一个场景,如果右侧布局是一个LinearLayout,我可以通过监听LinearLayout上的touch事件来控制左侧布局的显示和隐藏。但是如果右侧布局的LinearLayout里面加入了一个ListView,而这个ListView又充满了整个LinearLayout,这个时候LinearLayout将不可能再被touch到了,这个时候我们就需要将touch事件注册到ListView上。setScrollEvent方法也就是提供了一个注册接口,touch事件将会注册到传入的View上。

最后还有一个陌生的方法,isBindBasicLayout。这个方法就是判断了一下注册touch事件的View是不是四个基本布局之一,如果是就返回true,否则返回false。这个方法在整个SlidingLayout中起着非常重要的作用,主要用于控制onTouch事件是返回true还是false,这将影响到布局当中的View的功能是否可用。由于里面牵扯到了Android的事件转发机制,内容比较多,就不在这里详细解释了,我会考虑以后专门写一篇文章来介绍Android的事件机制。这里就先简单记住如果是基本布局就返回true,否则就返回false。

好了,我们的SlidingLayout写完了,接下来就是见证奇迹的时刻,让我们一起看看如何一分钟在Activity中引入滑动菜单功能。

创建或打开layout目录下的activity_main.xml文件,加入如下代码:

  1. <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:layout_width="fill_parent"
  4. android:layout_height="fill_parent"
  5. android:orientation="horizontal"
  6. tools:context=".MainActivity">
  7. <!--使用自定义的侧滑布局,orientation必须为水平方向-->
  8. <com.example.slide.SlidingLayout
  9. android:id="@+id/slidingLayout"
  10. android:layout_width="fill_parent"
  11. android:layout_height="fill_parent"
  12. android:orientation="horizontal">
  13. <!--
  14. 侧滑布局的根节点下,有且只能有两个子元素,这两个子元素必须是四种基本布局之一,
  15. 即LinearLayout,RelativeLayout,FrameLayout或TableLayout。
  16. 第一个子元素将做为左侧布局,初始化后被隐藏。第二个子元素将做为右侧布局,
  17. 也就是当前Activity的主布局,将主要的数据放在里面。
  18. -->
  19. <RelativeLayout
  20. android:id="@+id/menu"
  21. android:layout_width="fill_parent"
  22. android:layout_height="fill_parent"
  23. android:background="#00ccff">
  24. <TextView
  25. android:layout_width="wrap_content"
  26. android:layout_height="wrap_content"
  27. android:layout_centerInParent="true"
  28. android:text="Thisismenu"
  29. android:textColor="#000000"
  30. android:textSize="28sp"/>
  31. </RelativeLayout>
  32. <LinearLayout
  33. android:id="@+id/content"
  34. android:layout_width="fill_parent"
  35. android:layout_height="fill_parent"
  36. android:orientation="vertical">
  37. <Button
  38. android:id="@+id/menuButton"
  39. android:layout_width="wrap_content"
  40. android:layout_height="wrap_content"
  41. android:text="Menu"/>
  42. <ListView
  43. android:id="@+id/contentList"
  44. android:layout_width="fill_parent"
  45. android:layout_height="fill_parent">
  46. </ListView>
  47. </LinearLayout>
  48. </com.example.slide.SlidingLayout>
  49. </LinearLayout>
我们可以看到,在根布局的下面,我们引入了自定义布局com.example.slide.SlidingLayout,然后在它里面加入了两个子元素,一个RelativeLayout和一个LinearLayout。RelativeLayout中比较简单,就加入了一个TextView。LinearLayout里面我们加入了一个按钮和一个ListView。

然后创建或打开MainActivity作为程序的主Activity,加入代码:

  1. publicclassMainActivityextendsActivity{
  2. /**
  3. *侧滑布局对象,用于通过手指滑动将左侧的菜单布局进行显示或隐藏。
  4. */
  5. privateSlidingLayoutslidingLayout;
  6. /**
  7. *menu按钮,点击按钮展示左侧布局,再点击一次隐藏左侧布局。
  8. */
  9. privateButtonmenuButton;
  10. /**
  11. *放在content布局中的ListView。
  12. */
  13. privateListViewcontentListView;
  14. /**
  15. *作用于contentListView的适配器。
  16. */
  17. privateArrayAdapter<String>contentListAdapter;
  18. /**
  19. *用于填充contentListAdapter的数据源。
  20. */
  21. privateString[]contentItems={"ContentItem1","ContentItem2","ContentItem3",
  22. "ContentItem4","ContentItem5","ContentItem6","ContentItem7",
  23. "ContentItem8","ContentItem9","ContentItem10","ContentItem11",
  24. "ContentItem12","ContentItem13","ContentItem14","ContentItem15",
  25. "ContentItem16"};
  26. @Override
  27. protectedvoidonCreate(BundlesavedInstanceState){
  28. super.onCreate(savedInstanceState);
  29. setContentView(R.layout.activity_main);
  30. slidingLayout=(SlidingLayout)findViewById(R.id.slidingLayout);
  31. menuButton=(Button)findViewById(R.id.menuButton);
  32. contentListView=(ListView)findViewById(R.id.contentList);
  33. contentListAdapter=newArrayAdapter<String>(this,android.R.layout.simple_list_item_1,
  34. contentItems);
  35. contentListView.setAdapter(contentListAdapter);
  36. //将监听滑动事件绑定在contentListView上
  37. slidingLayout.setScrollEvent(contentListView);
  38. menuButton.setOnClickListener(newOnClickListener(){
  39. @Override
  40. publicvoidonClick(Viewv){
  41. //实现点击一下menu展示左侧布局,再点击一下隐藏左侧布局的功能
  42. if(slidingLayout.isLeftLayoutVisible()){
  43. slidingLayout.scrollToRightLayout();
  44. }else{
  45. slidingLayout.scrollToLeftLayout();
  46. }
  47. }
  48. });
  49. }
  50. }
上述代码重点是调用SlidingLayout的setScrollEvent方法,为ListView注册touch事件。同时给按钮添加了一个点击事件,实现了点击一下显示左边布局,再点击一下隐藏左边布局的功能。

最后还是老规矩,给出AndroidManifest.xml的代码:

  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <manifestxmlns:android="http://schemas.android.com/apk/res/android"
  3. package="com.example.slide"
  4. android:versionCode="1"
  5. android:versionName="1.0">
  6. <uses-sdk
  7. android:minSdkVersion="8"
  8. android:targetSdkVersion="8"/>
  9. <application
  10. android:allowBackup="true"
  11. android:icon="@drawable/ic_launcher"
  12. android:label="@string/app_name"
  13. android:theme="@android:style/Theme.NoTitleBar">
  14. <activity
  15. android:name="com.example.slide.MainActivity"
  16. android:label="@string/app_name">
  17. <intent-filter>
  18. <actionandroid:name="android.intent.action.MAIN"/>
  19. <categoryandroid:name="android.intent.category.LAUNCHER"/>
  20. </intent-filter>
  21. </activity>
  22. </application>
  23. </manifest>
好了,现在让我们运行一下吧。首先是程序打开的时候,显示的是右边布局。用手指在界面上向右滑动,可以看到左边布局出现。


而当左边布局完全显示的时候,效果图如下:


除此之外,点击Menu按钮也可以控制左边布局的显示和隐藏,大家可以自己试一下。

使用自定义布局的话,就可以用简单的方式在任意Activity中加入滑动菜单功能,即使你有再多的Activity也不用怕了,一分钟引入滑动菜单妥妥的。

再总结一下吧,向Activity中加入滑动菜单功能只需要两步:

1. 在Acitivty的layout中引入我们自定义的布局,并且给这个布局要加入两个直接子元素。

2. 在Activity中通过setScrollEvent方法,给一个View注册touch事件。

好了,今天的讲解到此结束,有疑问的朋友请在下面留言。

源码下载,请点击这里

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics