Android仿人人客户端(v5.7.1)——采用RelativeLayout做父容器,实现左侧滑动菜单(一)

转载请标明出处:http://blog.csdn.net/android_ls/article/details/8756059

一、滑动效果的实现原理:

      1、采用RelativeLayout作为父容器, 当调用addView(View child)方法向其中添加子View(子View采用FrameLayout),并且其子View的布局参数都设置的是填充整个父容器的大小(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT))。那么父容器中当前显示的应该是最后添加的View。

      2、 要实现子View可以在父容器中滑动,那么我们就得重写父容器的onInterceptTouchEvent(MotionEvent ev)和onTouchEvent(MotionEvent event)方法,拦截用户触屏手势并作出判断和事件处理。比如,当前用户的某个(单击、向左滑动和向右滑动等)触屏事件,是否需要响应,若要响应,是父容器自己去处理呢,还是应该交给父容器里的某个子View去处理。

      3、要做到第2小点中提到的,必须先了解ViewGrop的onInterceptTouchEvent(MotionEvent ev)和onTouchEvent(MotionEvent event)回调方法,在整个View树(应用框架)中的调用先后顺序及其返回值所代表的含义。下面做个小测试,前提:ViewGroup(父容器中的一个子View)有子View,并且子View中的View有事件处理器(比如,子View是Button,事件处理器指的就是Button的点击事件监听器中的onClick(View v)方法)或者子View可以获得焦点(比如选中效果)。

      自定义类继承RelativeLayout类,如下:

[java]  view plain copy
  1. public class ParentContainer extends RelativeLayout {  
  2.   
  3.     private static final String TAG = "ParentContainer";  
  4.   
  5.     public ParentContainer(Context context) {  
  6.         super(context);  
  7.     }  
  8.   
  9.     @Override  
  10.     public boolean onInterceptTouchEvent(MotionEvent ev) {  
  11.         Log.e(TAG, "ParentContainer : onInterceptTouchEvent()");  
  12.   
  13.         return super.onInterceptTouchEvent(ev);  
  14.     }  
  15.   
  16.     @Override  
  17.     public boolean onTouchEvent(MotionEvent event) {  
  18.         Log.e(TAG, "ParentContainer : onTouchEvent()");  
  19.   
  20.         return super.onTouchEvent(event);  
  21.     }  
  22.   
  23. }  

        其子View代码如下:

[java]  view plain copy
  1. public class ChildContainer extends FrameLayout {  
  2.   
  3.     private static final String TAG = "ChildContainer";  
  4.   
  5.     public ChildContainer(Context context) {  
  6.         super(context);  
  7.   
  8.         Button btnTest = new Button(context);  
  9.         btnTest.setText("测试按钮");  
  10.         btnTest.setOnClickListener(new View.OnClickListener() {  
  11.   
  12.             @Override  
  13.             public void onClick(View v) {  
  14.                 Log.i(TAG, "ChildContainer : 我响应了单击事件");  
  15.             }  
  16.         });  
  17.   
  18.         LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);  
  19.         this.addView(btnTest, params);  
  20.     }  
  21.   
  22.     @Override  
  23.     public boolean onInterceptTouchEvent(MotionEvent ev) {  
  24.         Log.i(TAG, "ChildContainer : onInterceptTouchEvent()");  
  25.   
  26.         return super.onInterceptTouchEvent(ev);  
  27.     }  
  28.   
  29.     @Override  
  30.     public boolean onTouchEvent(MotionEvent event) {  
  31.         Log.i(TAG, "ChildContainer : onTouchEvent()");  
  32.   
  33.         return super.onTouchEvent(event);  
  34.     }  
  35.   
  36. }  

     测试Activity代码:

[java]  view plain copy
  1. public class TestActivity extends Activity {  
  2.       
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.           
  7.         ParentContainer mSlideContainer = new ParentContainer(this);  
  8.           
  9.         ChildContainer childContainer = new ChildContainer(this);  
  10.         LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);  
  11.         mSlideContainer.addView(childContainer, params);  
  12.           
  13.         setContentView(mSlideContainer);  
  14.     }  
  15.       
  16. }  

     a. 默认情况下,单击子View中的Button按钮,LogCat打印Log如下:

Android仿人人客户端(v5.7.1)——采用RelativeLayout做父容器,实现左侧滑动菜单(一)_第1张图片

        修改父容器的onInterceptTouchEvent()返回值为false,单击子View的按钮,打印Log与默认值一样。跟踪源码发现其实默认返回值就是false。修改返回值为true,单击子View的按钮,LogCat打印Log如下:

       结论:父容器中onInterceptTouchEvent()方法的返回值为true时,表示将事件交给ViewGroup自己的onTouchEvent()去处理;返回值为false时,表示将事件交给ViewGroup的子View的onInterceptTouchEvent()去处理。(默认的处理方式)
      b. 父容器使用默认的值,修改子View的onTouchEvent()方法返回值为false,单击子View,LogCat打印Log如下:

Android仿人人客户端(v5.7.1)——采用RelativeLayout做父容器,实现左侧滑动菜单(一)_第2张图片

          修改子View的onTouchEvent()方法返回值为true,单击子View,LogCat打印Log如下:

Android仿人人客户端(v5.7.1)——采用RelativeLayout做父容器,实现左侧滑动菜单(一)_第3张图片

      结论:父容器使用默认的值,修改子View的onTouchEvent()方法返回值为false,表示将事件交父View处理;修改子View的onTouchEvent()方法返回值为true,表示该事件子View自己已经处理了,到这里终止。
         4、在父容器中,拦截用户触屏手势后,想交给父容器自己去处理,或者是想交给父容器里的某个子View去处理,应该怎么实现,通过上面的讲解,我想大家已经明白了,决定事件的传递顺序或在那个View里终止传递,是通过ViewGroup中的onInterceptTouchEvent(MotionEvent ev)和onTouchEvent(MotionEvent event)方法的返回值来决定的。接下来开始聊如何实现父容器中的子View的左右滑动(滚动),这里就用到了Scroller和VelocityTracker两个类。

       a. 为什么要用Scroller类? 如果实现想把一个View偏移至指定坐标(x,y)处,利用View类提供的scrollTo()方法直接调用就可以了。但是View类的scrollTo()方法是非常迅速的将View从一个坐标点(20, 0)移到另一个坐标点(300, 0),而没有对这个偏移过程有任何控制,对用户而言这件事发生的很突然,用户体验不好。而Scroller类提供的startScroll()方法,在偏移过程中添加了动画,提升了用户体验。因此我们选择使用Scroller类的对象来实现View的偏移。

       b. VelocityTracker类,主要用跟踪触摸屏事件(flinging事件和其他gestures手势事件)的速率。  用addMovement(MotionEvent)函数将Motion event加入到VelocityTracker类实例中。你可以使用getXVelocity() 或getXVelocity()获得横向和竖向的速率到速率时,但是使用它们之前请先调用computeCurrentVelocity(int)来初始化速率的单位 。

       关于computeCurrentVelocity(int units, float maxVelocity) 方法的参数列表解释:

       int  unitis表示速率的基本时间单位。unitis值为1的表示是,一毫秒时间单位内运动了多少个像素, unitis值为1000表示一秒(1000毫秒)时间单位内运动了多少个像素。

       float  maxVelocity表示速率的最大值。

二、按上面的讲解思路编码实现:

       1、滑动方式实现:

                 只在父容器的onInterceptTouchEvent(MotionEvent ev)和onTouchEvent(MotionEvent event)方法中,添加MotionEvent.ACTION_MOVE的事件处理。
          父容器类代码如下:

[java]  view plain copy
  1. package com.everyone.android.widget;  
  2.   
  3. import android.content.Context;  
  4. import android.util.Log;  
  5. import android.util.TypedValue;  
  6. import android.view.MotionEvent;  
  7. import android.view.VelocityTracker;  
  8. import android.view.ViewConfiguration;  
  9. import android.widget.RelativeLayout;  
  10. import android.widget.Scroller;  
  11.   
  12. /** 
  13.  * 功能描述:手指在屏幕上左右滑动时,该类的实例负责其的子View的左右偏移(滚动) 
  14.  * @author android_ls 
  15.  */  
  16. public class ScrollerContainer extends RelativeLayout {  
  17.   
  18.     private static final String TAG = "ScrollerContainer";  
  19.   
  20.     private Scroller mScroller;  
  21.   
  22.     private VelocityTracker mVelocityTracker;  
  23.   
  24.     /** 
  25.      * 手柄(手把)的宽度 
  26.      */  
  27.     private int mHandlebarWidth;  
  28.   
  29.     /** 
  30.      * 一秒时间内移动了多少个像素 
  31.      */  
  32.     private float mVelocityValue;  
  33.       
  34.     /** 
  35.      * 在偏移过程中,动画持续的时间 
  36.      */  
  37.     private static final int ANIMATION_DURATION_TIME = 300;  
  38.       
  39.     public ScrollerContainer(Context context) {  
  40.         super(context);  
  41.   
  42.         mScroller = new Scroller(context);  
  43.         mHandlebarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 70, getResources().getDisplayMetrics());  
  44.     }  
  45.   
  46.     @Override  
  47.     public boolean onInterceptTouchEvent(MotionEvent ev) {  
  48.         Log.e(TAG, "ParentContainer : onInterceptTouchEvent()");  
  49.   
  50.         mVelocityTracker = VelocityTracker.obtain();  
  51.         mVelocityTracker.addMovement(ev);  
  52.           
  53.         switch (ev.getAction()) {  
  54.         case MotionEvent.ACTION_DOWN:  
  55.             Log.i(TAG, "onInterceptTouchEvent():  ACTION_DOWN");  
  56.               
  57.             break;  
  58.   
  59.         case MotionEvent.ACTION_MOVE:  
  60.             Log.i(TAG, "onInterceptTouchEvent():  ACTION_MOVE");  
  61.               
  62.             mVelocityTracker.computeCurrentVelocity(1000, ViewConfiguration.getMaximumFlingVelocity());  
  63.             mVelocityValue = Math.abs(mVelocityTracker.getXVelocity()) ;  
  64.             Log.d(TAG, "onInterceptTouchEvent():  mVelocityValue = " + mVelocityValue);  
  65.               
  66.             if (mVelocityValue > 300) {  
  67.                 return true;  
  68.             }  
  69.               
  70.             break;  
  71.   
  72.         case MotionEvent.ACTION_UP:  
  73.             Log.i(TAG, "onInterceptTouchEvent():  ACTION_UP");  
  74.             break;  
  75.         case MotionEvent.ACTION_CANCEL:  
  76.             Log.i(TAG, "onInterceptTouchEvent():  ACTION_CANCEL");  
  77.             break;  
  78.         default:  
  79.             break;  
  80.         }  
  81.           
  82.         return super.onInterceptTouchEvent(ev);  
  83.     }  
  84.   
  85.     @Override  
  86.     public boolean onTouchEvent(MotionEvent event) {  
  87.         Log.e(TAG, "ParentContainer : onTouchEvent()");  
  88.   
  89.         float x = event.getX();  
  90.           
  91.         switch (event.getAction()) {  
  92.         case MotionEvent.ACTION_DOWN:  
  93.             Log.i(TAG, "onTouchEvent():  ACTION_DOWN");  
  94.               
  95.             break;  
  96.   
  97.         case MotionEvent.ACTION_MOVE:  
  98.             Log.i(TAG, "onTouchEvent():  ACTION_MOVE");  
  99.               
  100.             getChildAt(1).scrollTo(-(int)x, 0);  
  101.             break;  
  102.   
  103.         case MotionEvent.ACTION_UP:  
  104.             Log.i(TAG, "onTouchEvent():  ACTION_UP");  
  105.               
  106.            
  107.            float width = getWidth();  
  108.            float halfWidth = width / 2;  
  109.              
  110.            Log.i(TAG, "onTouchEvent():  ACTION_UP x = " + x + "\t halfWidth = " + halfWidth);  
  111.              
  112.            int scrollX = getChildAt(1).getScrollX();  
  113.              
  114.            if ( x < halfWidth) {  
  115.                Log.i(TAG, "onTouchEvent():  ACTION_UP 向左滑动");  
  116.                  
  117.                 mScroller.startScroll(scrollX, 0, -scrollX, 0, ANIMATION_DURATION_TIME);  
  118.                 invalidate();  
  119.             } else if ( x > halfWidth){  
  120.                 Log.i(TAG, "onTouchEvent():  ACTION_UP 向右滑动");  
  121.                 
  122.                 int toX = (int)(width - Math.abs(scrollX) - mHandlebarWidth);  
  123.                 mScroller.startScroll(scrollX, 0, -toX, 0, ANIMATION_DURATION_TIME);  
  124.                 invalidate();  
  125.             }  
  126.               
  127.             break;  
  128.         case MotionEvent.ACTION_CANCEL:  
  129.             Log.i(TAG, "onTouchEvent():  ACTION_CANCEL");  
  130.             break;  
  131.         default:  
  132.             break;  
  133.         }  
  134.           
  135.         return super.onTouchEvent(event);  
  136.     }  
  137.   
  138.     @Override  
  139.     public void computeScroll() {  
  140.         // super.computeScroll();  
  141.           
  142.         if(mScroller.computeScrollOffset()){  
  143.             this.getChildAt(1).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
  144.             this.postInvalidate();  
  145.         }  
  146.     }  
  147.   
  148. }  

         子容器就是两个继承自FrameLayout的Layout,源码就不贴了。

        测试类代码如下:

[java]  view plain copy
  1. package com.everyone.android.ui;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5. import android.view.ViewGroup.LayoutParams;  
  6.   
  7. import com.everyone.android.widget.FreshNewsLayout;  
  8. import com.everyone.android.widget.LeftPanelLayout;  
  9. import com.everyone.android.widget.ScrollerContainer;  
  10.   
  11. public class TestActivity extends Activity {  
  12.   
  13.     /** 
  14.      * 左侧面板 
  15.      */  
  16.     private LeftPanelLayout mLeftPanelLayout;  
  17.   
  18.     /** 
  19.      * 新鲜事 
  20.      */  
  21.     private FreshNewsLayout mFreshNewsLayout;  
  22.   
  23.     @Override  
  24.     public void onCreate(Bundle savedInstanceState) {  
  25.         super.onCreate(savedInstanceState);  
  26.   
  27.         ScrollerContainer mSlideContainer = new ScrollerContainer(this);  
  28.         LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);  
  29.   
  30.         mLeftPanelLayout = new LeftPanelLayout(this.getApplicationContext());  
  31.         mFreshNewsLayout = new FreshNewsLayout(this.getApplicationContext());  
  32.   
  33.         mSlideContainer.addView(mLeftPanelLayout, params);  
  34.         mSlideContainer.addView(mFreshNewsLayout, params);  
  35.   
  36.         setContentView(mSlideContainer);  
  37.     }  
  38.   
  39. }  

         运行效果图如下:

Android仿人人客户端(v5.7.1)——采用RelativeLayout做父容器,实现左侧滑动菜单(一)_第4张图片

        2、单击方式实现:

       左右滑动实现子View的滚动,有一个临界值,一秒时间内移动了的像素数要大于某个预设的值,才会触动相应事件处理器。那么为了用户体验好点,我们提供另外一种操作方式,那就是单击事件。假设ViewGroup中有两个子View A和B,B处于A上面,两个子View是叠在一起的。默认显示的是B,并占据着整个手机屏幕,A是看不见的。为了能看见A,并且可以操作,我们需要把B视图(子View)移动一定单位,可以在B视图中添加子View(Button)并绑定事件监听器。当用户点击B视图中的特定子View(Button)时,让B视图偏移一定单位,我么就能看见A视图;当B处于A上并偏移了一定的单位,这时单击B,实现B视图移动到回去(恢复默认显示)。

       a. 单击B,实现B视图移动到回去(恢复默认显示),代码如下:

[java]  view plain copy
  1.     @Override  
  2.     public boolean onInterceptTouchEvent(MotionEvent ev) {  
  3.         Log.e(TAG, "ParentContainer : onInterceptTouchEvent()");  
  4.   
  5.         mVelocityTracker = VelocityTracker.obtain();  
  6.         mVelocityTracker.addMovement(ev);  
  7.           
  8.         switch (ev.getAction()) {  
  9.         case MotionEvent.ACTION_DOWN:  
  10.             Log.i(TAG, "onInterceptTouchEvent():  ACTION_DOWN");  
  11.               
  12.             int x = (int) ev.getX();  
  13.             int width = getWidth();  
  14.             if(x >= (width - mHandlebarWidth)){  
  15.                 isClick = true;  
  16.             }  
  17.               
  18.             break;  
  19.   
  20.         case MotionEvent.ACTION_MOVE:  
  21.             Log.i(TAG, "onInterceptTouchEvent():  ACTION_MOVE");  
  22.               
  23.             mVelocityTracker.computeCurrentVelocity(1000, ViewConfiguration.getMaximumFlingVelocity());  
  24.             mVelocityValue = Math.abs(mVelocityTracker.getXVelocity()) ;  
  25.             Log.d(TAG, "onInterceptTouchEvent():  mVelocityValue = " + mVelocityValue);  
  26.               
  27.             if (mVelocityValue > 300) {  
  28.                 return true;  
  29.             }  
  30.               
  31.             break;  
  32.   
  33.         case MotionEvent.ACTION_UP:  
  34.             Log.i(TAG, "onInterceptTouchEvent():  ACTION_UP");  
  35.               
  36.             if (isClick) {  
  37.                 isClick = false;  
  38.                  int scrollX = getChildAt(1).getScrollX();  
  39.                  mScroller.startScroll(scrollX, 0, -scrollX, 0, ANIMATION_DURATION_TIME);                                                        invalidate();  
  40.             }  
  41.               
  42.             break;  
  43.         case MotionEvent.ACTION_CANCEL:  
  44.             Log.i(TAG, "onInterceptTouchEvent():  ACTION_CANCEL");  
  45.             break;  
  46.         default:  
  47.             break;  
  48.         }  
  49.           
  50.         return super.onInterceptTouchEvent(ev);  
  51.     }  


         b. 当用户点击B视图中的特定子View(Button)时,让B视图偏移一定单位。在父容器中添加滑动事件监听器和向右滑动的实现,代码如下:

[java]  view plain copy
  1. /** 
  2.    * 向右滑动View,让左侧操作面饭可见 
  3.    */  
  4.   public void slideToRight() {  
  5.       float width = getWidth();  
  6.       int scrollX = getChildAt(1).getScrollX();  
  7.       int toX = (int)(width - Math.abs(scrollX) - mHandlebarWidth);  
  8.       mScroller.startScroll(scrollX, 0, -toX, 0, ANIMATION_DURATION_TIME);  
  9.       invalidate();  
  10.   }  
  11.     
  12.   /** 
  13.    * View滑动事件监听器 
  14.    * @author android_ls 
  15.    */  
  16.   public interface OnSlideListener {  
  17.       /** 
  18.        * 向做滑动View 
  19.        */  
  20.       public abstract void toLeft();  
  21.         
  22.       /** 
  23.        * 向右滑动View 
  24.        */  
  25.       public abstract void toRight();  
  26.   }  

         子视图FreshNewsLayout的源码:

[java]  view plain copy
  1. package com.everyone.android.widget;  
  2.   
  3. import android.content.Context;  
  4. import android.util.AttributeSet;  
  5. import android.view.LayoutInflater;  
  6. import android.view.View;  
  7. import android.widget.FrameLayout;  
  8. import android.widget.LinearLayout;  
  9.   
  10. import com.everyone.android.R;  
  11. import com.everyone.android.widget.ScrollerContainer.OnSlideListener;  
  12.   
  13. /** 
  14.  * 功能描述:新鲜事视图 
  15.  * @author android_ls 
  16.  */  
  17. public class FreshNewsLayout extends FrameLayout {  
  18.   
  19.     public LinearLayout llBack;  
  20.       
  21.     private OnSlideListener mOnSlideListener;  
  22.       
  23.     public FreshNewsLayout(Context context) {  
  24.         super(context);  
  25.         setupViews();  
  26.     }  
  27.   
  28.     public FreshNewsLayout(Context context, AttributeSet attrs) {  
  29.         super(context, attrs);  
  30.         setupViews();  
  31.     }  
  32.   
  33.     public void setOnSlideListener(OnSlideListener onSlideListener) {  
  34.         mOnSlideListener = onSlideListener;  
  35.     }  
  36.       
  37.     private void setupViews() {  
  38.         final LayoutInflater mLayoutInflater = LayoutInflater.from(getContext());  
  39.         LinearLayout rlTopNavbar = (LinearLayout) mLayoutInflater.inflate(R.layout.fresh_news, null);  
  40.         addView(rlTopNavbar);  
  41.   
  42.         llBack = (LinearLayout) rlTopNavbar.findViewById(R.id.ll_back);  
  43.         llBack.setOnClickListener(new OnClickListener() {  
  44.   
  45.             public void onClick(View v) {  
  46.                 if (mOnSlideListener != null) {  
  47.                     mOnSlideListener.toRight();  
  48.                 }  
  49.             }  
  50.         });  
  51.           
  52.     }  
  53.   
  54. }  

         测试类代码如下:

[java]  view plain copy
  1. package com.everyone.android.ui;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5. import android.view.ViewGroup.LayoutParams;  
  6.   
  7. import com.everyone.android.widget.FreshNewsLayout;  
  8. import com.everyone.android.widget.LeftPanelLayout;  
  9. import com.everyone.android.widget.ScrollerContainer;  
  10. import com.everyone.android.widget.ScrollerContainer.OnSlideListener;  
  11.   
  12. public class TestActivity extends Activity implements OnSlideListener {  
  13.   
  14.     /** 
  15.      * 左侧面板 
  16.      */  
  17.     private LeftPanelLayout mLeftPanelLayout;  
  18.   
  19.     /** 
  20.      * 新鲜事 
  21.      */  
  22.     private FreshNewsLayout mFreshNewsLayout;  
  23.   
  24.     /** 
  25.      * 滚动(滑动)容器 
  26.      */  
  27.     private ScrollerContainer mSlideContainer;  
  28.   
  29.     @Override  
  30.     public void onCreate(Bundle savedInstanceState) {  
  31.         super.onCreate(savedInstanceState);  
  32.   
  33.         mSlideContainer = new ScrollerContainer(this);  
  34.         LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);  
  35.   
  36.         mLeftPanelLayout = new LeftPanelLayout(this.getApplicationContext());  
  37.         mFreshNewsLayout = new FreshNewsLayout(this.getApplicationContext());  
  38.         mFreshNewsLayout.setOnSlideListener(this);  
  39.   
  40.         mSlideContainer.addView(mLeftPanelLayout, params);  
  41.         mSlideContainer.addView(mFreshNewsLayout, params);  
  42.   
  43.         setContentView(mSlideContainer);  
  44.     }  
  45.   
  46.     @Override  
  47.     public void toLeft() {  
  48.         // TODO Auto-generated method stub  
  49.   
  50.     }  
  51.   
  52.     @Override  
  53.     public void toRight() {  
  54.         mSlideContainer.slideToRight();  
  55.     }  
  56.   
  57. }  

你可能感兴趣的:(Android仿人人客户端(v5.7.1)——采用RelativeLayout做父容器,实现左侧滑动菜单(一))