Android双向滑动菜单完全解析,教你如何一分钟实现双向滑动特效
转载请注明出处:http://blog.****.net/guolin_blog/article/details/9671609
记得在很早之前,我写了一篇关于Android滑动菜单的文章,其中有一个朋友在评论中留言,希望我可以帮他将这个滑动菜单改成双向滑动的方式。当时也没想花太多时间,简单修改了一下就发给了他,结果没想到后来却有一大批的朋友都来问我要这份双向滑动菜单的代码。由于这份代码写得很不用心,我发了部分朋友之后实在不忍心继续发下去了,于是决定专门写一篇文章来介绍更好的Android双向滑动菜单的实现方法。
在开始动手之前先来讲一下实现原理,在一个Activity的布局中需要有三部分,一个是左侧菜单的布局,一个是右侧菜单的布局,一个是内容布局。左侧菜单居屏幕左边缘对齐,右侧菜单居屏幕右边缘对齐,然后内容布局占满整个屏幕,并压在了左侧菜单和右侧菜单的上面。当用户手指向右滑动时,将右侧菜单隐藏,左侧菜单显示,然后通过偏移内容布局的位置,就可以让左侧菜单展现出来。同样的道理,当用户手指向左滑动时,将左侧菜单隐藏,右侧菜单显示,也是通过偏移内容布局的位置,就可以让右侧菜单展现出来。原理示意图所下所示:
介绍完了原理,我们就开始动手实现吧。新建一个Android项目,项目名就叫做BidirSlidingLayout。然后新建我们最主要的BidirSlidingLayout类,这个类就是实现双向滑动菜单功能的核心类,代码如下所示:
- publicclassBidirSlidingLayoutextendsRelativeLayoutimplementsOnTouchListener{
- /**
- *滚动显示和隐藏左侧布局时,手指滑动需要达到的速度。
- */
- publicstaticfinalintSNAP_VELOCITY=200;
- /**
- *滑动状态的一种,表示未进行任何滑动。
- */
- publicstaticfinalintDO_NOTHING=0;
- /**
- *滑动状态的一种,表示正在滑出左侧菜单。
- */
- publicstaticfinalintSHOW_LEFT_MENU=1;
- /**
- *滑动状态的一种,表示正在滑出右侧菜单。
- */
- publicstaticfinalintSHOW_RIGHT_MENU=2;
- /**
- *滑动状态的一种,表示正在隐藏左侧菜单。
- */
- publicstaticfinalintHIDE_LEFT_MENU=3;
- /**
- *滑动状态的一种,表示正在隐藏右侧菜单。
- */
- publicstaticfinalintHIDE_RIGHT_MENU=4;
- /**
- *记录当前的滑动状态
- */
- privateintslideState;
- /**
- *屏幕宽度值。
- */
- privateintscreenWidth;
- /**
- *在被判定为滚动之前用户手指可以移动的最大值。
- */
- privateinttouchSlop;
- /**
- *记录手指按下时的横坐标。
- */
- privatefloatxDown;
- /**
- *记录手指按下时的纵坐标。
- */
- privatefloatyDown;
- /**
- *记录手指移动时的横坐标。
- */
- privatefloatxMove;
- /**
- *记录手指移动时的纵坐标。
- */
- privatefloatyMove;
- /**
- *记录手机抬起时的横坐标。
- */
- privatefloatxUp;
- /**
- *左侧菜单当前是显示还是隐藏。只有完全显示或隐藏时才会更改此值,滑动过程中此值无效。
- */
- privatebooleanisLeftMenuVisible;
- /**
- *右侧菜单当前是显示还是隐藏。只有完全显示或隐藏时才会更改此值,滑动过程中此值无效。
- */
- privatebooleanisRightMenuVisible;
- /**
- *是否正在滑动。
- */
- privatebooleanisSliding;
- /**
- *左侧菜单布局对象。
- */
- privateViewleftMenuLayout;
- /**
- *右侧菜单布局对象。
- */
- privateViewrightMenuLayout;
- /**
- *内容布局对象。
- */
- privateViewcontentLayout;
- /**
- *用于监听滑动事件的View。
- */
- privateViewmBindView;
- /**
- *左侧菜单布局的参数。
- */
- privateMarginLayoutParamsleftMenuLayoutParams;
- /**
- *右侧菜单布局的参数。
- */
- privateMarginLayoutParamsrightMenuLayoutParams;
- /**
- *内容布局的参数。
- */
- privateRelativeLayout.LayoutParamscontentLayoutParams;
- /**
- *用于计算手指滑动的速度。
- */
- privateVelocityTrackermVelocityTracker;
- /**
- *重写BidirSlidingLayout的构造函数,其中获取了屏幕的宽度和touchSlop的值。
- *
- *@paramcontext
- *@paramattrs
- */
- publicBidirSlidingLayout(Contextcontext,AttributeSetattrs){
- super(context,attrs);
- WindowManagerwm=(WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
- screenWidth=wm.getDefaultDisplay().getWidth();
- touchSlop=ViewConfiguration.get(context).getScaledTouchSlop();
- }
- /**
- *绑定监听滑动事件的View。
- *
- *@parambindView
- *需要绑定的View对象。
- */
- publicvoidsetScrollEvent(ViewbindView){
- mBindView=bindView;
- mBindView.setOnTouchListener(this);
- }
- /**
- *将界面滚动到左侧菜单界面,滚动速度设定为-30.
- */
- publicvoidscrollToLeftMenu(){
- newLeftMenuScrollTask().execute(-30);
- }
- /**
- *将界面滚动到右侧菜单界面,滚动速度设定为-30.
- */
- publicvoidscrollToRightMenu(){
- newRightMenuScrollTask().execute(-30);
- }
- /**
- *将界面从左侧菜单滚动到内容界面,滚动速度设定为30.
- */
- publicvoidscrollToContentFromLeftMenu(){
- newLeftMenuScrollTask().execute(30);
- }
- /**
- *将界面从右侧菜单滚动到内容界面,滚动速度设定为30.
- */
- publicvoidscrollToContentFromRightMenu(){
- newRightMenuScrollTask().execute(30);
- }
- /**
- *左侧菜单是否完全显示出来,滑动过程中此值无效。
- *
- *@return左侧菜单完全显示返回true,否则返回false。
- */
- publicbooleanisLeftLayoutVisible(){
- returnisLeftMenuVisible;
- }
- /**
- *右侧菜单是否完全显示出来,滑动过程中此值无效。
- *
- *@return右侧菜单完全显示返回true,否则返回false。
- */
- publicbooleanisRightLayoutVisible(){
- returnisRightMenuVisible;
- }
- /**
- *在onLayout中重新设定左侧菜单、右侧菜单、以及内容布局的参数。
- */
- @Override
- protectedvoidonLayout(booleanchanged,intl,intt,intr,intb){
- super.onLayout(changed,l,t,r,b);
- if(changed){
- //获取左侧菜单布局对象
- leftMenuLayout=getChildAt(0);
- leftMenuLayoutParams=(MarginLayoutParams)leftMenuLayout.getLayoutParams();
- //获取右侧菜单布局对象
- rightMenuLayout=getChildAt(1);
- rightMenuLayoutParams=(MarginLayoutParams)rightMenuLayout.getLayoutParams();
- //获取内容布局对象
- contentLayout=getChildAt(2);
- contentLayoutParams=(RelativeLayout.LayoutParams)contentLayout.getLayoutParams();
- contentLayoutParams.width=screenWidth;
- contentLayout.setLayoutParams(contentLayoutParams);
- }
- }
- @Override
- publicbooleanonTouch(Viewv,MotionEventevent){
- createVelocityTracker(event);
- switch(event.getAction()){
- caseMotionEvent.ACTION_DOWN:
- //手指按下时,记录按下时的坐标
- xDown=event.getRawX();
- yDown=event.getRawY();
- //将滑动状态初始化为DO_NOTHING
- slideState=DO_NOTHING;
- break;
- caseMotionEvent.ACTION_MOVE:
- xMove=event.getRawX();
- yMove=event.getRawY();
- //手指移动时,对比按下时的坐标,计算出移动的距离。
- intmoveDistanceX=(int)(xMove-xDown);
- intmoveDistanceY=(int)(yMove-yDown);
- //检查当前的滑动状态
- checkSlideState(moveDistanceX,moveDistanceY);
- //根据当前滑动状态决定如何偏移内容布局
- switch(slideState){
- caseSHOW_LEFT_MENU:
- contentLayoutParams.rightMargin=-moveDistanceX;
- checkLeftMenuBorder();
- contentLayout.setLayoutParams(contentLayoutParams);
- break;
- caseHIDE_LEFT_MENU:
- contentLayoutParams.rightMargin=-leftMenuLayoutParams.width-moveDistanceX;
- checkLeftMenuBorder();
- contentLayout.setLayoutParams(contentLayoutParams);
- caseSHOW_RIGHT_MENU:
- contentLayoutParams.leftMargin=moveDistanceX;
- checkRightMenuBorder();
- contentLayout.setLayoutParams(contentLayoutParams);
- break;
- caseHIDE_RIGHT_MENU:
- contentLayoutParams.leftMargin=-rightMenuLayoutParams.width+moveDistanceX;
- checkRightMenuBorder();
- contentLayout.setLayoutParams(contentLayoutParams);
- default:
- break;
- }
- break;
- caseMotionEvent.ACTION_UP:
- xUp=event.getRawX();
- intupDistanceX=(int)(xUp-xDown);
- if(isSliding){
- //手指抬起时,进行判断当前手势的意图
- switch(slideState){
- caseSHOW_LEFT_MENU:
- if(shouldScrollToLeftMenu()){
- scrollToLeftMenu();
- }else{
- scrollToContentFromLeftMenu();
- }
- break;
- caseHIDE_LEFT_MENU:
- if(shouldScrollToContentFromLeftMenu()){
- scrollToContentFromLeftMenu();
- }else{
- scrollToLeftMenu();
- }
- break;
- caseSHOW_RIGHT_MENU:
- if(shouldScrollToRightMenu()){
- scrollToRightMenu();
- }else{
- scrollToContentFromRightMenu();
- }
- break;
- caseHIDE_RIGHT_MENU:
- if(shouldScrollToContentFromRightMenu()){
- scrollToContentFromRightMenu();
- }else{
- scrollToRightMenu();
- }
- break;
- default:
- break;
- }
- }elseif(upDistanceX<touchSlop&&isLeftMenuVisible){
- //当左侧菜单显示时,如果用户点击一下内容部分,则直接滚动到内容界面
- scrollToContentFromLeftMenu();
- }elseif(upDistanceX<touchSlop&&isRightMenuVisible){
- //当右侧菜单显示时,如果用户点击一下内容部分,则直接滚动到内容界面
- scrollToContentFromRightMenu();
- }
- recycleVelocityTracker();
- break;
- }
- if(v.isEnabled()){
- if(isSliding){
- //正在滑动时让控件得不到焦点
- unFocusBindView();
- returntrue;
- }
- if(isLeftMenuVisible||isRightMenuVisible){
- //当左侧或右侧布局显示时,将绑定控件的事件屏蔽掉
- returntrue;
- }
- returnfalse;
- }
- returntrue;
- }
- /**
- *根据手指移动的距离,判断当前用户的滑动意图,然后给slideState赋值成相应的滑动状态值。
- *
- *@parammoveDistanceX
- *横向移动的距离
- *@parammoveDistanceY
- *纵向移动的距离
- */
- privatevoidcheckSlideState(intmoveDistanceX,intmoveDistanceY){
- if(isLeftMenuVisible){
- if(!isSliding&&Math.abs(moveDistanceX)>=touchSlop&&moveDistanceX<0){
- isSliding=true;
- slideState=HIDE_LEFT_MENU;
- }
- }elseif(isRightMenuVisible){
- if(!isSliding&&Math.abs(moveDistanceX)>=touchSlop&&moveDistanceX>0){
- isSliding=true;
- slideState=HIDE_RIGHT_MENU;
- }
- }else{
- if(!isSliding&&Math.abs(moveDistanceX)>=touchSlop&&moveDistanceX>0
- &&Math.abs(moveDistanceY)<touchSlop){
- isSliding=true;
- slideState=SHOW_LEFT_MENU;
- contentLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT,0);
- contentLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
- contentLayout.setLayoutParams(contentLayoutParams);
- //如果用户想要滑动左侧菜单,将左侧菜单显示,右侧菜单隐藏
- leftMenuLayout.setVisibility(View.VISIBLE);
- rightMenuLayout.setVisibility(View.GONE);
- }elseif(!isSliding&&Math.abs(moveDistanceX)>=touchSlop&&moveDistanceX<0
- &&Math.abs(moveDistanceY)<touchSlop){
- isSliding=true;
- slideState=SHOW_RIGHT_MENU;
- contentLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,0);
- contentLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
- contentLayout.setLayoutParams(contentLayoutParams);
- //如果用户想要滑动右侧菜单,将右侧菜单显示,左侧菜单隐藏
- rightMenuLayout.setVisibility(View.VISIBLE);
- leftMenuLayout.setVisibility(View.GONE);
- }
- }
- }
- /**
- *在滑动过程中检查左侧菜单的边界值,防止绑定布局滑出屏幕。
- */
- privatevoidcheckLeftMenuBorder(){
- if(contentLayoutParams.rightMargin>0){
- contentLayoutParams.rightMargin=0;
- }elseif(contentLayoutParams.rightMargin<-leftMenuLayoutParams.width){
- contentLayoutParams.rightMargin=-leftMenuLayoutParams.width;
- }
- }
- /**
- *在滑动过程中检查右侧菜单的边界值,防止绑定布局滑出屏幕。
- */
- privatevoidcheckRightMenuBorder(){
- if(contentLayoutParams.leftMargin>0){
- contentLayoutParams.leftMargin=0;
- }elseif(contentLayoutParams.leftMargin<-rightMenuLayoutParams.width){
- contentLayoutParams.leftMargin=-rightMenuLayoutParams.width;
- }
- }
- /**
- *判断是否应该滚动将左侧菜单展示出来。如果手指移动距离大于左侧菜单宽度的1/2,或者手指移动速度大于SNAP_VELOCITY,
- *就认为应该滚动将左侧菜单展示出来。
- *
- *@return如果应该将左侧菜单展示出来返回true,否则返回false。
- */
- privatebooleanshouldScrollToLeftMenu(){
- returnxUp-xDown>leftMenuLayoutParams.width/2||getScrollVelocity()>SNAP_VELOCITY;
- }
- /**
- *判断是否应该滚动将右侧菜单展示出来。如果手指移动距离大于右侧菜单宽度的1/2,或者手指移动速度大于SNAP_VELOCITY,
- *就认为应该滚动将右侧菜单展示出来。
- *
- *@return如果应该将右侧菜单展示出来返回true,否则返回false。
- */
- privatebooleanshouldScrollToRightMenu(){
- returnxDown-xUp>rightMenuLayoutParams.width/2||getScrollVelocity()>SNAP_VELOCITY;
- }
- /**
- *判断是否应该从左侧菜单滚动到内容布局,如果手指移动距离大于左侧菜单宽度的1/2,或者手指移动速度大于SNAP_VELOCITY,
- *就认为应该从左侧菜单滚动到内容布局。
- *
- *@return如果应该从左侧菜单滚动到内容布局返回true,否则返回false。
- */
- privatebooleanshouldScrollToContentFromLeftMenu(){
- returnxDown-xUp>leftMenuLayoutParams.width/2||getScrollVelocity()>SNAP_VELOCITY;
- }
- /**
- *判断是否应该从右侧菜单滚动到内容布局,如果手指移动距离大于右侧菜单宽度的1/2,或者手指移动速度大于SNAP_VELOCITY,
- *就认为应该从右侧菜单滚动到内容布局。
- *
- *@return如果应该从右侧菜单滚动到内容布局返回true,否则返回false。
- */
- privatebooleanshouldScrollToContentFromRightMenu(){
- returnxUp-xDown>rightMenuLayoutParams.width/2||getScrollVelocity()>SNAP_VELOCITY;
- }
- /**
- *创建VelocityTracker对象,并将触摸事件加入到VelocityTracker当中。
- *
- *@paramevent
- *右侧布局监听控件的滑动事件
- */
- privatevoidcreateVelocityTracker(MotionEventevent){
- if(mVelocityTracker==null){
- mVelocityTracker=VelocityTracker.obtain();
- }
- mVelocityTracker.addMovement(event);
- }
- /**
- *获取手指在绑定布局上的滑动速度。
- *
- *@return滑动速度,以每秒钟移动了多少像素值为单位。
- */
- privateintgetScrollVelocity(){
- mVelocityTracker.computeCurrentVelocity(1000);
- intvelocity=(int)mVelocityTracker.getXVelocity();
- returnMath.abs(velocity);
- }
- /**
- *回收VelocityTracker对象。
- */
- privatevoidrecycleVelocityTracker(){
- mVelocityTracker.recycle();
- mVelocityTracker=null;
- }
- /**
- *使用可以获得焦点的控件在滑动的时候失去焦点。
- */
- privatevoidunFocusBindView(){
- if(mBindView!=null){
- mBindView.setPressed(false);
- mBindView.setFocusable(false);
- mBindView.setFocusableInTouchMode(false);
- }
- }
- classLeftMenuScrollTaskextendsAsyncTask<Integer,Integer,Integer>{
- @Override
- protectedIntegerdoInBackground(Integer...speed){
- intrightMargin=contentLayoutParams.rightMargin;
- //根据传入的速度来滚动界面,当滚动到达边界值时,跳出循环。
- while(true){
- rightMargin=rightMargin+speed[0];
- if(rightMargin<-leftMenuLayoutParams.width){
- rightMargin=-leftMenuLayoutParams.width;
- break;
- }
- if(rightMargin>0){
- rightMargin=0;
- break;
- }
- publishProgress(rightMargin);
- //为了要有滚动效果产生,每次循环使线程睡眠一段时间,这样肉眼才能够看到滚动动画。
- sleep(15);
- }
- if(speed[0]>0){
- isLeftMenuVisible=false;
- }else{
- isLeftMenuVisible=true;
- }
- isSliding=false;
- returnrightMargin;
- }
- @Override
- protectedvoidonProgressUpdate(Integer...rightMargin){
- contentLayoutParams.rightMargin=rightMargin[0];
- contentLayout.setLayoutParams(contentLayoutParams);
- unFocusBindView();
- }
- @Override
- protectedvoidonPostExecute(IntegerrightMargin){
- contentLayoutParams.rightMargin=rightMargin;
- contentLayout.setLayoutParams(contentLayoutParams);
- }
- }
- classRightMenuScrollTaskextendsAsyncTask<Integer,Integer,Integer>{
- @Override
- protectedIntegerdoInBackground(Integer...speed){
- intleftMargin=contentLayoutParams.leftMargin;
- //根据传入的速度来滚动界面,当滚动到达边界值时,跳出循环。
- while(true){
- leftMargin=leftMargin+speed[0];
- if(leftMargin<-rightMenuLayoutParams.width){
- leftMargin=-rightMenuLayoutParams.width;
- break;
- }
- if(leftMargin>0){
- leftMargin=0;
- break;
- }
- publishProgress(leftMargin);
- //为了要有滚动效果产生,每次循环使线程睡眠一段时间,这样肉眼才能够看到滚动动画。
- sleep(15);
- }
- if(speed[0]>0){
- isRightMenuVisible=false;
- }else{
- isRightMenuVisible=true;
- }
- isSliding=false;
- returnleftMargin;
- }
- @Override
- protectedvoidonProgressUpdate(Integer...leftMargin){
- contentLayoutParams.leftMargin=leftMargin[0];
- contentLayout.setLayoutParams(contentLayoutParams);
- unFocusBindView();
- }
- @Override
- protectedvoidonPostExecute(IntegerleftMargin){
- contentLayoutParams.leftMargin=leftMargin;
- contentLayout.setLayoutParams(contentLayoutParams);
- }
- }
- /**
- *使当前线程睡眠指定的毫秒数。
- *
- *@parammillis
- *指定当前线程睡眠多久,以毫秒为单位
- */
- privatevoidsleep(longmillis){
- try{
- Thread.sleep(millis);
- }catch(InterruptedExceptione){
- e.printStackTrace();
- }
- }
- }
然后我们看一下setScrollEvent方法,这个方法接收一个View作为参数,然后为这个View绑定了一个touch事件。这是什么意思呢?让我们来想象一个场景,如果内容布局是一个LinearLayout,我可以通过监听LinearLayout上的touch事件来控制它的偏移。但是如果内容布局的LinearLayout里面加入了一个ListView,而这个ListView又充满了整个LinearLayout,这个时候LinearLayout将不可能再被touch到了,这个时候我们就需要将touch事件注册到ListView上。setScrollEvent方法也就是提供了一个注册接口,touch事件将会注册到传入的View上。
接下来打开或新建activity_main.xml文件,加入如下代码:
- <com.example.bidirslidinglayout.BidirSlidingLayoutxmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/bidir_sliding_layout"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent">
- <RelativeLayout
- android:id="@+id/left_menu"
- android:layout_width="270dip"
- android:layout_height="fill_parent"
- android:layout_alignParentLeft="true"
- android:background="#00ccff"
- android:visibility="invisible">
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:text="Thisisleftmenu"
- android:textColor="#000000"
- android:textSize="28sp"/>
- </RelativeLayout>
- <RelativeLayout
- android:id="@+id/right_menu"
- android:layout_width="270dip"
- android:layout_height="fill_parent"
- android:layout_alignParentRight="true"
- android:background="#00ffcc"
- android:visibility="invisible">
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:text="Thisisrightmenu"
- android:textColor="#000000"
- android:textSize="28sp"/>
- </RelativeLayout>
- <LinearLayout
- android:id="@+id/content"
- android:layout_width="320dip"
- android:layout_height="fill_parent"
- android:background="#e9e9e9">
- <ListView
- android:id="@+id/contentList"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:scrollbars="none"
- android:cacheColorHint="#00000000">
- </ListView>
- </LinearLayout>
- </com.example.bidirslidinglayout.BidirSlidingLayout>
最后打开或者创建MainActivity作为程序的主Activity,代码如下所示:
- publicclassMainActivityextendsActivity{
- /**
- *双向滑动菜单布局
- */
- privateBidirSlidingLayoutbidirSldingLayout;
- /**
- *在内容布局上显示的ListView
- */
- privateListViewcontentList;
- /**
- *ListView的适配器
- */
- privateArrayAdapter<String>contentListAdapter;
- /**
- *用于填充contentListAdapter的数据源。
- */
- privateString[]contentItems={"ContentItem1","ContentItem2","ContentItem3",
- "ContentItem4","ContentItem5","ContentItem6","ContentItem7",
- "ContentItem8","ContentItem9","ContentItem10","ContentItem11",
- "ContentItem12","ContentItem13","ContentItem14","ContentItem15",
- "ContentItem16"};
- @Override
- protectedvoidonCreate(BundlesavedInstanceState){
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- bidirSldingLayout=(BidirSlidingLayout)findViewById(R.id.bidir_sliding_layout);
- contentList=(ListView)findViewById(R.id.contentList);
- contentListAdapter=newArrayAdapter<String>(this,android.R.layout.simple_list_item_1,
- contentItems);
- contentList.setAdapter(contentListAdapter);
- bidirSldingLayout.setScrollEvent(contentList);
- }
- }
好了,全部编码工作都已完成,现在让我们运行一下程序吧,效果如下图所示:
看起来还是挺不错的吧!并且更重要的是,以后我们在项目的任何地方都可以轻松加入双向滑动菜单功能,只需要以下两步即可:
1.在Acitivty的layout中引入我们自定义的BidirSlidingLayout布局,并且给这个布局要加入三个直接子元素。
2.在Activity中通过setScrollEvent方法,给一个View注册touch事件。
如此一来,一分钟实现双向滑动菜单功能妥妥的。
好了,今天的讲解到此结束,有疑问的朋友请在下面留言。