转载请标明出处: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类,如下:
- public class ParentContainer extends RelativeLayout {
-
- private static final String TAG = "ParentContainer";
-
- public ParentContainer(Context context) {
- super(context);
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- Log.e(TAG, "ParentContainer : onInterceptTouchEvent()");
-
- return super.onInterceptTouchEvent(ev);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- Log.e(TAG, "ParentContainer : onTouchEvent()");
-
- return super.onTouchEvent(event);
- }
-
- }
其子View代码如下:
- public class ChildContainer extends FrameLayout {
-
- private static final String TAG = "ChildContainer";
-
- public ChildContainer(Context context) {
- super(context);
-
- Button btnTest = new Button(context);
- btnTest.setText("测试按钮");
- btnTest.setOnClickListener(new View.OnClickListener() {
-
- @Override
- public void onClick(View v) {
- Log.i(TAG, "ChildContainer : 我响应了单击事件");
- }
- });
-
- LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
- this.addView(btnTest, params);
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- Log.i(TAG, "ChildContainer : onInterceptTouchEvent()");
-
- return super.onInterceptTouchEvent(ev);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- Log.i(TAG, "ChildContainer : onTouchEvent()");
-
- return super.onTouchEvent(event);
- }
-
- }
测试Activity代码:
- public class TestActivity extends Activity {
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- ParentContainer mSlideContainer = new ParentContainer(this);
-
- ChildContainer childContainer = new ChildContainer(this);
- LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
- mSlideContainer.addView(childContainer, params);
-
- setContentView(mSlideContainer);
- }
-
- }
a. 默认情况下,单击子View中的Button按钮,LogCat打印Log如下:
修改父容器的onInterceptTouchEvent()返回值为false,单击子View的按钮,打印Log与默认值一样。跟踪源码发现其实默认返回值就是false。修改返回值为true,单击子View的按钮,LogCat打印Log如下:
结论:父容器中onInterceptTouchEvent()方法的返回值为true时,表示将事件交给ViewGroup自己的onTouchEvent()去处理;返回值为false时,表示将事件交给ViewGroup的子View的onInterceptTouchEvent()去处理。(默认的处理方式)
b. 父容器使用默认的值,修改子View的onTouchEvent()方法返回值为false,单击子View,LogCat打印Log如下:
修改子View的onTouchEvent()方法返回值为true,单击子View,LogCat打印Log如下:
结论:父容器使用默认的值,修改子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的事件处理。
父容器类代码如下:
- package com.everyone.android.widget;
-
- import android.content.Context;
- import android.util.Log;
- import android.util.TypedValue;
- import android.view.MotionEvent;
- import android.view.VelocityTracker;
- import android.view.ViewConfiguration;
- import android.widget.RelativeLayout;
- import android.widget.Scroller;
-
-
-
-
-
- public class ScrollerContainer extends RelativeLayout {
-
- private static final String TAG = "ScrollerContainer";
-
- private Scroller mScroller;
-
- private VelocityTracker mVelocityTracker;
-
-
-
-
- private int mHandlebarWidth;
-
-
-
-
- private float mVelocityValue;
-
-
-
-
- private static final int ANIMATION_DURATION_TIME = 300;
-
- public ScrollerContainer(Context context) {
- super(context);
-
- mScroller = new Scroller(context);
- mHandlebarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 70, getResources().getDisplayMetrics());
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- Log.e(TAG, "ParentContainer : onInterceptTouchEvent()");
-
- mVelocityTracker = VelocityTracker.obtain();
- mVelocityTracker.addMovement(ev);
-
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- Log.i(TAG, "onInterceptTouchEvent(): ACTION_DOWN");
-
- break;
-
- case MotionEvent.ACTION_MOVE:
- Log.i(TAG, "onInterceptTouchEvent(): ACTION_MOVE");
-
- mVelocityTracker.computeCurrentVelocity(1000, ViewConfiguration.getMaximumFlingVelocity());
- mVelocityValue = Math.abs(mVelocityTracker.getXVelocity()) ;
- Log.d(TAG, "onInterceptTouchEvent(): mVelocityValue = " + mVelocityValue);
-
- if (mVelocityValue > 300) {
- return true;
- }
-
- break;
-
- case MotionEvent.ACTION_UP:
- Log.i(TAG, "onInterceptTouchEvent(): ACTION_UP");
- break;
- case MotionEvent.ACTION_CANCEL:
- Log.i(TAG, "onInterceptTouchEvent(): ACTION_CANCEL");
- break;
- default:
- break;
- }
-
- return super.onInterceptTouchEvent(ev);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- Log.e(TAG, "ParentContainer : onTouchEvent()");
-
- float x = event.getX();
-
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- Log.i(TAG, "onTouchEvent(): ACTION_DOWN");
-
- break;
-
- case MotionEvent.ACTION_MOVE:
- Log.i(TAG, "onTouchEvent(): ACTION_MOVE");
-
- getChildAt(1).scrollTo(-(int)x, 0);
- break;
-
- case MotionEvent.ACTION_UP:
- Log.i(TAG, "onTouchEvent(): ACTION_UP");
-
-
- float width = getWidth();
- float halfWidth = width / 2;
-
- Log.i(TAG, "onTouchEvent(): ACTION_UP x = " + x + "\t halfWidth = " + halfWidth);
-
- int scrollX = getChildAt(1).getScrollX();
-
- if ( x < halfWidth) {
- Log.i(TAG, "onTouchEvent(): ACTION_UP 向左滑动");
-
- mScroller.startScroll(scrollX, 0, -scrollX, 0, ANIMATION_DURATION_TIME);
- invalidate();
- } else if ( x > halfWidth){
- Log.i(TAG, "onTouchEvent(): ACTION_UP 向右滑动");
-
- int toX = (int)(width - Math.abs(scrollX) - mHandlebarWidth);
- mScroller.startScroll(scrollX, 0, -toX, 0, ANIMATION_DURATION_TIME);
- invalidate();
- }
-
- break;
- case MotionEvent.ACTION_CANCEL:
- Log.i(TAG, "onTouchEvent(): ACTION_CANCEL");
- break;
- default:
- break;
- }
-
- return super.onTouchEvent(event);
- }
-
- @Override
- public void computeScroll() {
-
-
- if(mScroller.computeScrollOffset()){
- this.getChildAt(1).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
- this.postInvalidate();
- }
- }
-
- }
子容器就是两个继承自FrameLayout的Layout,源码就不贴了。
测试类代码如下:
- package com.everyone.android.ui;
-
- import android.app.Activity;
- import android.os.Bundle;
- import android.view.ViewGroup.LayoutParams;
-
- import com.everyone.android.widget.FreshNewsLayout;
- import com.everyone.android.widget.LeftPanelLayout;
- import com.everyone.android.widget.ScrollerContainer;
-
- public class TestActivity extends Activity {
-
-
-
-
- private LeftPanelLayout mLeftPanelLayout;
-
-
-
-
- private FreshNewsLayout mFreshNewsLayout;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- ScrollerContainer mSlideContainer = new ScrollerContainer(this);
- LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
-
- mLeftPanelLayout = new LeftPanelLayout(this.getApplicationContext());
- mFreshNewsLayout = new FreshNewsLayout(this.getApplicationContext());
-
- mSlideContainer.addView(mLeftPanelLayout, params);
- mSlideContainer.addView(mFreshNewsLayout, params);
-
- setContentView(mSlideContainer);
- }
-
- }
运行效果图如下:
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视图移动到回去(恢复默认显示),代码如下:
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- Log.e(TAG, "ParentContainer : onInterceptTouchEvent()");
-
- mVelocityTracker = VelocityTracker.obtain();
- mVelocityTracker.addMovement(ev);
-
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- Log.i(TAG, "onInterceptTouchEvent(): ACTION_DOWN");
-
- int x = (int) ev.getX();
- int width = getWidth();
- if(x >= (width - mHandlebarWidth)){
- isClick = true;
- }
-
- break;
-
- case MotionEvent.ACTION_MOVE:
- Log.i(TAG, "onInterceptTouchEvent(): ACTION_MOVE");
-
- mVelocityTracker.computeCurrentVelocity(1000, ViewConfiguration.getMaximumFlingVelocity());
- mVelocityValue = Math.abs(mVelocityTracker.getXVelocity()) ;
- Log.d(TAG, "onInterceptTouchEvent(): mVelocityValue = " + mVelocityValue);
-
- if (mVelocityValue > 300) {
- return true;
- }
-
- break;
-
- case MotionEvent.ACTION_UP:
- Log.i(TAG, "onInterceptTouchEvent(): ACTION_UP");
-
- if (isClick) {
- isClick = false;
- int scrollX = getChildAt(1).getScrollX();
- mScroller.startScroll(scrollX, 0, -scrollX, 0, ANIMATION_DURATION_TIME); invalidate();
- }
-
- break;
- case MotionEvent.ACTION_CANCEL:
- Log.i(TAG, "onInterceptTouchEvent(): ACTION_CANCEL");
- break;
- default:
- break;
- }
-
- return super.onInterceptTouchEvent(ev);
- }
b. 当用户点击B视图中的特定子View(Button)时,让B视图偏移一定单位。在父容器中添加滑动事件监听器和向右滑动的实现,代码如下:
-
-
-
- public void slideToRight() {
- float width = getWidth();
- int scrollX = getChildAt(1).getScrollX();
- int toX = (int)(width - Math.abs(scrollX) - mHandlebarWidth);
- mScroller.startScroll(scrollX, 0, -toX, 0, ANIMATION_DURATION_TIME);
- invalidate();
- }
-
-
-
-
-
- public interface OnSlideListener {
-
-
-
- public abstract void toLeft();
-
-
-
-
- public abstract void toRight();
- }
子视图FreshNewsLayout的源码:
- package com.everyone.android.widget;
-
- import android.content.Context;
- import android.util.AttributeSet;
- import android.view.LayoutInflater;
- import android.view.View;
- import android.widget.FrameLayout;
- import android.widget.LinearLayout;
-
- import com.everyone.android.R;
- import com.everyone.android.widget.ScrollerContainer.OnSlideListener;
-
-
-
-
-
- public class FreshNewsLayout extends FrameLayout {
-
- public LinearLayout llBack;
-
- private OnSlideListener mOnSlideListener;
-
- public FreshNewsLayout(Context context) {
- super(context);
- setupViews();
- }
-
- public FreshNewsLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- setupViews();
- }
-
- public void setOnSlideListener(OnSlideListener onSlideListener) {
- mOnSlideListener = onSlideListener;
- }
-
- private void setupViews() {
- final LayoutInflater mLayoutInflater = LayoutInflater.from(getContext());
- LinearLayout rlTopNavbar = (LinearLayout) mLayoutInflater.inflate(R.layout.fresh_news, null);
- addView(rlTopNavbar);
-
- llBack = (LinearLayout) rlTopNavbar.findViewById(R.id.ll_back);
- llBack.setOnClickListener(new OnClickListener() {
-
- public void onClick(View v) {
- if (mOnSlideListener != null) {
- mOnSlideListener.toRight();
- }
- }
- });
-
- }
-
- }
测试类代码如下:
- package com.everyone.android.ui;
-
- import android.app.Activity;
- import android.os.Bundle;
- import android.view.ViewGroup.LayoutParams;
-
- import com.everyone.android.widget.FreshNewsLayout;
- import com.everyone.android.widget.LeftPanelLayout;
- import com.everyone.android.widget.ScrollerContainer;
- import com.everyone.android.widget.ScrollerContainer.OnSlideListener;
-
- public class TestActivity extends Activity implements OnSlideListener {
-
-
-
-
- private LeftPanelLayout mLeftPanelLayout;
-
-
-
-
- private FreshNewsLayout mFreshNewsLayout;
-
-
-
-
- private ScrollerContainer mSlideContainer;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mSlideContainer = new ScrollerContainer(this);
- LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
-
- mLeftPanelLayout = new LeftPanelLayout(this.getApplicationContext());
- mFreshNewsLayout = new FreshNewsLayout(this.getApplicationContext());
- mFreshNewsLayout.setOnSlideListener(this);
-
- mSlideContainer.addView(mLeftPanelLayout, params);
- mSlideContainer.addView(mFreshNewsLayout, params);
-
- setContentView(mSlideContainer);
- }
-
- @Override
- public void toLeft() {
-
-
- }
-
- @Override
- public void toRight() {
- mSlideContainer.slideToRight();
- }
-
- }