现在的位置: 首页 > 综合 > 正文

Android自定义ViewPager(一)——自定义Scroller模拟动画过程

2017年10月01日 ⁄ 综合 ⁄ 共 9770字 ⁄ 字号 评论关闭

       转载请注明出处:http://blog.csdn.net/allen315410/article/details/41575831

       相信Android SDK提供的ViewPager组件,大家实在是熟悉不过了,但是ViewPager存在于support.v4包下的,说明ViewPager并不存在于早期的android版本中,那么如何在早期的android版本中也同样使用类似于ViewPager一样的滑动效果呢?这里,我们还是继续探讨一下andrid的自定义组件好了,并且这篇博文只探讨android的一些知识,并不是刻意去构建一个自定义的ViewPager去使用,这个是没有必要的,请将注意力集中在实现这个效果的知识点上,方便以后“举一反三”。

       好了,我们先来简单分析一下ViewPager。ViewPager可以看做是一个“容器”,在这个“容器”里可以摆放各种各样的View类型,例如ViewPager每个分页上可以放置TextView,ImageView,ListView、GridView等等一系列View组件,实际上这些View在ViewPager上的摆放我们可以看做是在ViewGroup上Layout各种View(实际上,这个实现是比较复杂的,这里做个比喻意义而已),所以我们就可以抽象理解为,ViewPager相当于ViewGroup,并且在这个ViewGroup上Layout各种View,所以接下来的代码中,我们主要需要一个自定义的ViewGroup来实现达到这样的效果。另外,还需要在这个ViewGroup上给每个分页上的View添加一个左右滑动的效果,以求模拟出ViewPager上的动态效果。

       关于自定义ViewGroup的结构,我们有必要仔细探讨一下,某些概念还是值得去加深理解的,为了理解方便,请参看下面的“草图”:

         从上面的草图可以看到,红色的边框代表设备屏幕,即我们可以用肉眼看见的地方,整个灰色的大边框代表整个效果,这里称为“视图”,每个视图又分为3个View,这个3个或者多个View组成一张很大的视图。我们要弄清楚,这三者的关系,设备屏幕代表的显示区域,即我们在设备上能看见的范围,View代表的是单个的组件,一个屏幕上可以显示一个或者多个View,但是视图是最容易混淆的东西,视图理论上是很大的一块区域,它不但包括设备屏幕上能被肉眼看见的一部分,还包括设备屏幕以外肉眼看不见的地方,就如上图所示的,子View2和子View3也是视图的一部分,但是在设备屏幕之外,就是肉眼看不见的区域了。视图里可以存放很多的View,视图被用来管理View的显示效果。而且,视图是可以自由活动的,通过控制视图的活动,控制视图在设备屏幕上的显示范围,就可以切换不同的分页了。
      

       所以接下来,我们主要去做的就是如何去自定义一个视图,如何让视图展示不同的View在设备屏幕上,在Android上管理多个View的显示可以通过自定义的ViewGroup,实现onLayout给View进行排版,初始化排版的时候,我一共向ViewGroup里添加了6个子View,这6个子View呈水平横向排版,如上图所示的那样,每个View显示的宽度和高度跟父View(ViewGroup)相同,首次排版呈现出第一个子View在屏幕上,其他5个子View以次添加进来,以父View的宽度的N倍数排版,都被隐藏在设备屏幕的右边区域。下面是自定义ViewGroup的实现代码:

package com.example.myviewpager;

import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

public class MyViewPager extends ViewGroup {

	/** 手势识别器 */
	private GestureDetector detector;
	/** 上下文 */
	private Context ctx;
	/** 第一次按下的X轴的坐标 */
	private int firstDownX;
	/** 记录当前View的id */
	private int currId = 0;
	/** 模拟动画工具 */
	private MyScroller myScroller;

	public MyViewPager(Context context, AttributeSet attrs) {
		super(context, attrs);
		this.ctx = context;
		init();
	}

	private void init() {
		myScroller = new MyScroller(ctx);
		detector = new GestureDetector(ctx,
				new GestureDetector.OnGestureListener() {

					@Override
					public boolean onSingleTapUp(MotionEvent e) {
						return false;
					}

					@Override
					public void onShowPress(MotionEvent e) {
					}

					@Override
					public boolean onScroll(MotionEvent e1, MotionEvent e2,
							float distanceX, float distanceY) {
						// 手指滑动
						scrollBy((int) distanceX, 0);
						return false;
					}

					@Override
					public void onLongPress(MotionEvent e) {
					}

					@Override
					public boolean onFling(MotionEvent e1, MotionEvent e2,
							float velocityX, float velocityY) {
						return false;
					}

					@Override
					public boolean onDown(MotionEvent e) {
						return false;
					}
				});
	}

	/**
	 * 对子View进行布局,确定子View的位置 changed 若为true,
	 * 说明布局发生了变化 l\t\r\b 指当前View位于父View的位置
	 */
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		for (int i = 0; i < getChildCount(); i++) {
			View view = getChildAt(i);
			// 指定子View的位置 ,左、上、右、下,是指在ViewGroup坐标系中的位置
			view.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(),
					getHeight());
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		detector.onTouchEvent(event); // 指定手势识别器去处理滑动事件
		// 还是得自己处理一些逻辑
		switch (event.getAction()) {
			case MotionEvent.ACTION_DOWN : // 按下
				firstDownX = (int) event.getX();
				break;
			case MotionEvent.ACTION_MOVE : // 移动
				break;
			case MotionEvent.ACTION_UP : // 抬起
				int nextId = 0; // 记录下一个View的id
				if (event.getX() - firstDownX > getWidth() / 2) {
					// 手指离开点的X轴坐标-firstDownX > 屏幕宽度的一半,左移
					nextId = (currId - 1) <= 0 ? 0 : currId - 1;
				} else if (firstDownX - event.getX() > getWidth() / 2) {
					// 手指离开点的X轴坐标 - firstDownX < 屏幕宽度的一半,右移
					nextId = currId + 1;
				} else {
					nextId = currId;
				}

				moveToDest(nextId);
				break;
			default :
				break;
		}
		return true;
	}

	/**
	 * 控制视图的移动
	 * 
	 * @param nextId
	 */
	private void moveToDest(int nextId) {
		// nextId的合理范围是,nextId >=0 && nextId <= getChildCount()-1
		currId = (nextId >= 0) ? nextId : 0;
		currId = (nextId <= getChildCount() - 1)
				? nextId
				: (getChildCount() - 1);

		// 视图移动,太直接了,没有动态过程
		// scrollTo(currId * getWidth(), 0);
		// 要移动的距离 = 最终的位置 - 现在的位置
		int distanceX = currId * getWidth() - getScrollX();
		// 设置运行的时间
		myScroller.startScroll(getScrollX(), 0, distanceX, 0);
		// 刷新视图
		invalidate();
	}

	/**
	 * invalidate();会导致这个方法的执行
	 */
	@Override
	public void computeScroll() {
		if (myScroller.computeOffset()) {
			int newX = (int) myScroller.getCurrX();
			System.out.println("newX::" + newX);
			scrollTo(newX, 0);
			invalidate();
		}
	}

}

        1,上面是自定义ViewGroup的所有源码,接下来我们慢慢分析一下实现过程,首先是初始化各个子View的排版,上面已经说过了,主要代码在onLayout()方法中已经体现,比较简单。

        2,实现手势滑动效果。众所周知,ViewPager可以随着手指在屏幕上滑动而改变不同的分页,为了实现同样的效果,我在自定义ViewGroup中重写了父类的onTouchEvent(MotionEvent event)方法,该方法被用来处理滑动事件的逻辑。但是为了简便起见,我用了手势识别器GestureDetector,用这个手指识别器来处理手指在屏幕上移动时,视图跟着手指一起移动的效果,简单在GestureDetector的onScroll()方法中,将移动的距离传递给ScrollBy(int)作为参数即可。

        3,处理比较复杂的手指按下到抬起时,视图切换。这是一个具体分析的过程,下面是这个过程中涉及的"草图":

这里,我们以子View2这个View做示例来分析一下3种情况:

(1),手指离开点的X轴坐标 - 手指按下点的X轴坐标 > 屏幕宽度的一半,左移,屏幕显示下一个View

(2),手指离开点的X轴坐标 - 手指按下点的X轴坐标 < 屏幕宽度的一半,右移,屏幕显示上一个View

(3),以上两种条件都不满足,那就停留在当前View上,不切换前后View

       4,通过(3)的过程,我们就知道当前视图向哪一个View方向上移动了,得到下一个需要显示View的id,将这个id置为当前View的id,然后将下一个需要显示的View的id*View的宽度,传递给ScrollTo(int,0)作为参数,来控制视图的移动。

       5,通过以上步骤,View视图的切换就已经完成了,但是有个问题,在View的左右切换时使用了ScrollTo(int,int)方法,这个方法将View直接移动到指定的位置,但是整个移动的过程太过于迅速,一瞬间就完成了View的切换,这样的体验效果非常差,那么我们怎么提升体验效果呢?对了,是在这个View的切换给一个慢速的过程,让View切换的过程缓慢或者匀速的进行,这样体验效果就提生上去了,那么怎样在切换的过程中增加一个匀速的切换的效果呢?我们不妨先举下面一个小例子,方便理解:

       假如,有个人小A要走完一个100米的小路,他自己可以慢慢的走过去,用时很多,也可以一下子跑过去,用时极短,但是他想不紧不慢的匀速走完这段小路,该怎么办呢?这时候他找来了一位工程师小B,让工程师小B在旁边帮他计算路程,小A在前进前询问一下工程师小B,接下来5秒钟,我要走多少米啊?工程师小B就开始计算出结果,并且告诉小A,你先前进10米好了;当小A走完这个10米的路程时,小A又问小B,接下来5秒钟我要前进多少米的距离?小B一顿计算,告诉小A前进20米好了,于是小A继续前进20米,停下来接着问小B......反复此过程,知道小A走完这100米的小路为止。


       上面的例子不难理解吧!于是,在View的切换过程中,我们也需要这样的一位“工程师”时刻计算每一定时间间隔内的位移,传递给View视图,视图得到这个位移,就立马移动到相应的位置,再次请求“工程师”计算下,下一时间间隔内前进的位移,以此类推。下面,是我们自定义的一个计算位移的工具类源码:

package com.example.myviewpager;

import android.content.Context;
import android.os.SystemClock;

/**
 * 计算视图偏移的工具类
 * 
 * @author Administrator
 * 
 */
public class MyScroller {

	/** 开始时的X坐标 */
	private int startX;
	/** 开始时的Y坐标 */
	private int startY;
	/** X方向上要移动的距离 */
	private int distanceX;
	/** Y方向上要移动的距离 */
	private int distanceY;
	/** 开始的时间 */
	private long startTime;
	/** 移动是否结束 */
	private boolean isFinish;
	/** 当前X轴的坐标 */
	private long currX;
	/** 当前Y轴的坐标 */
	private long currY;
	/** 默认的时间间隔 */
	private int duration = 500;

	public MyScroller(Context ctx) {

	}

	/**
	 * 开始移动
	 * 
	 * @param startX
	 *            开始时的X坐标
	 * @param startY
	 *            开始时的Y坐标
	 * @param distanceX
	 *            X方向上要移动的距离
	 * @param distanceY
	 *            Y方向上要移动的距离
	 */
	public void startScroll(int startX, int startY, int distanceX, int distanceY) {
		this.startX = startX;
		this.startY = startY;
		this.distanceX = distanceX;
		this.distanceY = distanceY;
		this.startTime = SystemClock.uptimeMillis();
		this.isFinish = false;
	}

	/**
	 * 判断当前运行状态
	 * 
	 * @return
	 */
	public boolean computeOffset() {

		if (isFinish) {
			return false;
		}
		// 获得所用的时间
		long passTime = SystemClock.uptimeMillis() - startTime;
		System.out.println("passTime::" + passTime);
		// 如果时间还在允许的范围内
		if (passTime < duration) {
			currX = startX + distanceX * passTime / duration;
			currY = startY + distanceY * passTime / duration;
		} else {
			currX = startX + distanceX;
			currY = startY + distanceY;
			isFinish = true;
		}

		return true;
	}

	/**
	 * 获取当前X的值
	 * 
	 * @return
	 */
	public long getCurrX() {
		return currX;
	}

	public void setCurrX(long currX) {
		this.currX = currX;
	}

	/**
	 * 获取当前Y的值
	 * 
	 * @return
	 */
	public long getCurrY() {
		return currY;
	}

	public void setCurrY(long currY) {
		this.currY = currY;
	}

}

分析一下,这个过程。

       当我们在计算出切换到下一个View的id时,就可以得到切换的距离了,公式:要移动的距离 = 最终的位置 - 现在的位置;得到这个移动距离之后,拿到这个距离和初始位置,告诉“工程师”——工具类MyScroller,这时候可以开始计算了,初始化代码如下:

// 要移动的距离 = 最终的位置 - 现在的位置
int distanceX = currId * getWidth() - getScrollX();
// 设置运行的时间
myScroller.startScroll(getScrollX(), 0, distanceX, 0);
// 刷新视图
invalidate();

       初始化完计算工具类之后,需要刷新当前视图了,调用invalidate()方法,这个方法会经过一系列连锁反应,事实上刷新视图是个很复杂的过程,这里不讲解了,一直直到触发computeScroll()方法,此时,我们需要重写父类的computeScroll()方法,在这个方法中,完成自己的一些操作:

/**
* invalidate();会导致这个方法的执行
*/
@Override
public void computeScroll() {
	if (myScroller.computeOffset()) {
		int newX = (int) myScroller.getCurrX();
		System.out.println("newX::" + newX);
		scrollTo(newX, 0);
		invalidate();
	}
}

       

       在这个方法里,首先调用一下工具类计算位移的方法computeOffset()方法,该方法首先判断一下视图移动是否完成,若完成返回false,若没有完成,先获取运动的时间间隔,如果当前运动的时间间隔在总时间间隔duration之内,那么通过时间间隔计算出这段时间间隔之后,视图实际移动到的位置,公式是:开始位置+总的距离/总的时间*本段移动时间间隔,如果当前运动的时间间隔超出了总的时间间隔,那么直接算出最后一次位置,公式:开始位置+移动距离。通过getCurrX得到本次位移的距离,即最新的位移距离,调用scrollTo(int,int)方法,移动视图到新的位置。最后再次递归调用invalidate()刷新当前视图,然后触发computeScroll()方法,继续上述步骤,直至超出规定的时间间隔,返回false后,视图的位移过程结束。

      在布局文件中这样引用:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.example.myviewpager.MyViewPager
        android:id="@+id/myviewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

     在MainActivity里需要给这个自定义的组件初始化几个View,为了方便起见,我全部初始化了ImageView,每个ImageView设置不同的背景图片:

package com.example.myviewpager;

import android.os.Bundle;
import android.widget.ImageView;
import android.app.Activity;

public class MainActivity extends Activity {

	private MyViewPager myViewPager;
	// 图片资源
	private int[] imageRes = new int[]{R.drawable.a1, R.drawable.a2,
			R.drawable.a3, R.drawable.a4, R.drawable.a5, R.drawable.a6};

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		myViewPager = (MyViewPager) findViewById(R.id.myviewpager);

		ImageView view;
		for (int i = 0; i < imageRes.length; i++) {
			view = new ImageView(this);
			view.setBackgroundResource(imageRes[i]);
			myViewPager.addView(view);
		}
	}

}

        此外,在这个例子程序中我自定义了一个MyScroller工具类来计算位移大小了,感觉费时费力,作为学习原理可行,但是实际开发中,可以使用Android为我们提供了类似的、极其简便的Helper类,可以使用这个Helper类来计算位移,这个类就是

android.widget.Scroller; 

以下是Scroller类的相关方法:  

mScroller.getCurrX()    //获取mScroller当前水平滚动的位置  
mScroller.getCurrY()    //获取mScroller当前竖直滚动的位置  
mScroller.getFinalX()   //获取mScroller最终停止的水平位置  
mScroller.getFinalY()     //获取mScroller最终停止的竖直位置  
mScroller.setFinalX(int newX)    //设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置  
mScroller.setFinalY(int newY)    //设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置  
mScroller.startScroll(int startX, int startY, int dx, int dy)   //滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量  
mScroller.startScroll(int startX, int startY, int dx, int dy, int duration)    //滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量, duration为完成滚动的时间
mScroller.computeScrollOffset()   //返回值为boolean,true说明滚动尚未完成,false说明滚动已经完成。这是一个很重要的方法,通常放在View.computeScroll()中,用来判断是否滚动是否结束。

       Scroller的具体使用实践在我的前面博文中有用过,请移步Android自定义控件——侧滑菜单查看相关源码。

源码请在这里下载

抱歉!评论已关闭.