Android自定义ViewPager(一)――自定义Scroller模拟动画过程
来源:程序员人生 发布时间:2014-12-10 08:32:22 阅读次数:2358次
转载请注明出处:http://blog.csdn.net/allen315410/article/details/41575831
相信Android SDK提供的ViewPager组件,大家实在是熟习不过了,但是ViewPager存在于support.v4包下的,说明ViewPager其实不存在于初期的android版本中,那末如何在初期的android版本中也一样使用类似于ViewPager1样的滑动效果呢?这里,我们还是继续探讨1下andrid的自定义组件好了,并且这篇博文只探讨android的1些知识,其实不是刻意去构建1个自定义的ViewPager去使用,这个是没有必要的,请将注意力集中在实现这个效果的知识点上,方便以后“举1反3”。
好了,我们先来简单分析1下ViewPager。ViewPager可以看作是1个“容器”,在这个“容器”里可以摆放各种各样的View类型,例如ViewPager每一个分页上可以放置TextView,ImageView,ListView、GridView等等1系列View组件,实际上这些View在ViewPager上的摆放我们可以看作是在ViewGroup上Layout各种View(实际上,这个实现是比较复杂的,这里做个比喻意义而已),所以我们就能够抽象理解为,ViewPager相当于ViewGroup,并且在这个ViewGroup上Layout各种View,所以接下来的代码中,我们主要需要1个自定义的ViewGroup来实现到达这样的效果。另外,还需要在这个ViewGroup上给每一个分页上的View添加1个左右滑动的效果,以求摹拟出ViewPager上的动态效果。
关于自定义ViewGroup的结构,我们有必要仔细探讨1下,某些概念还是值得去加深理解的,为了理解方便,请参看下面的“草图”:
从上面的草图可以看到,红色的边框代表装备屏幕,即我们可以用肉眼看见的地方,全部灰色的大边框代表全部效果,这里称为“视图”,每一个视图又分为3个View,这个3个或多个View组成1张很大的视图。我们要弄清楚,这3者的关系,装备屏幕代表的显示区域,即我们在装备上能看见的范围,View代表的是单个的组件,1个屏幕上可以显示1个或多个View,但是视图是最容易混淆的东西,视图理论上是很大的1块区域,它不但包括装备屏幕上能被肉眼看见的1部份,还包括装备屏幕之外肉眼看不见的地方,就如上图所示的,子View2和子View3也是视图的1部份,但是在装备屏幕以外,就是肉眼看不见的区域了。视图里可以寄存很多的View,视图被用来管理View的显示效果。而且,视图是可以自由活动的,通过控制视图的活动,控制视图在装备屏幕上的显示范围,就能够切换不同的分页了。
所以接下来,我们主要去做的就是如何去自定义1个视图,如何让视图展现不同的View在装备屏幕上,在Android上管理多个View的显示可以通过自定义的ViewGroup,实现onLayout给View进行排版,初始化排版的时候,我1共向ViewGroup里添加了6个子View,这6个子View呈水平横向排版,如上图所示的那样,每一个View显示的宽度和高度跟父View(ViewGroup)相同,首次排版显现出第1个子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;
/** 第1次按下的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
指当前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); // 指定手势辨认器去处理滑动事件
// 还是得自己处理1些逻辑
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN : // 按下
firstDownX = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE : // 移动
break;
case MotionEvent.ACTION_UP : // 抬起
int nextId = 0; // 记录下1个View的id
if (event.getX() - firstDownX > getWidth() / 2) {
// 手指离开点的X轴坐标-firstDownX > 屏幕宽度的1半,左移
nextId = (currId - 1) <= 0 ? 0 : currId - 1;
} else if (firstDownX - event.getX() > getWidth() / 2) {
// 手指离开点的X轴坐标 - firstDownX < 屏幕宽度的1半,右移
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()⑴
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的所有源码,接下来我们渐渐分析1下实现进程,首先是初始化各个子View的排版,上面已说过了,主要代码在onLayout()方法中已体现,比较简单。
2,实现手势滑动效果。尽人皆知,ViewPager可以随着手指在屏幕上滑动而改变不同的分页,为了实现一样的效果,我在自定义ViewGroup中重写了父类的onTouchEvent(MotionEvent event)方法,该方法被用来处理滑动事件的逻辑。但是为了简便起见,我用了手势辨认器GestureDetector,用这个手指辨认器来处理手指在屏幕上移动时,视图随着手指1起移动的效果,简单在GestureDetector的onScroll()方法中,将移动的距离传递给ScrollBy(int)作为参数便可。
3,处理比较复杂的手指按下到抬起时,视图切换。这是1个具体分析的进程,下面是这个进程中触及的"草图":
这里,我们以子View2这个View做示例来分析1下3种情况:
(1),手指离开点的X轴坐标 - 手指按下点的X轴坐标 > 屏幕宽度的1半,左移,屏幕显示下1个View
(2),手指离开点的X轴坐标 - 手指按下点的X轴坐标 < 屏幕宽度的1半,右移,屏幕显示上1个View
(3),以上两种条件都不满足,那就停留在当前View上,不切换前后View
4,通过(3)的进程,我们就知道当前视图向哪个View方向上移动了,得到下1个需要显示View的id,将这个id置为当前View的id,然后将下1个需要显示的View的id*View的宽度,传递给ScrollTo(int,0)作为参数,来控制视图的移动。
5,通过以上步骤,View视图的切换就已完成了,但是有个问题,在View的左右切换时使用了ScrollTo(int,int)方法,这个方法将View直接移动到指定的位置,但是全部移动的进程太过于迅速,1瞬间就完成了View的切换,这样的体验效果非常差,那末我们怎样提升体验效果呢?对了,是在这个View的切换给1个慢速的进程,让View切换的进程缓慢或匀速的进行,这样体验效果就提生上去了,那末怎样在切换的进程中增加1个匀速的切换的效果呢?我们无妨先举下面1个小例子,方便理解:
假设,有个人小A要走完1个100米的小路,他自己可以渐渐的走过去,用时很多,也能够1下子跑过去,用时极短,但是他想不紧不慢的匀速走完这段小路,该怎样办呢?这时候候他找来了1位工程师小B,让工程师小B在旁边帮他计算路程,小A在前进前询问1下工程师小B,接下来5秒钟,我要走多少米啊?工程师小B就开始计算出结果,并且告知小A,你先前进10米好了;当小A走完这个10米的路程时,小A又问小B,接下来5秒钟我要前进多少米的距离?小B1顿计算,告知小A前进20米好了,因而小A继续前进20米,停下来接着问小B......反复此进程,知道小A走完这100米的小路为止。
上面的例子不难理解吧!因而,在View的切换进程中,我们也需要这样的1位“工程师”时刻计算每定时间间隔内的位移,传递给View视图,视图得到这个位移,就立马移动到相应的位置,再次要求“工程师”计算下,下1时间间隔内前进的位移,以此类推。下面,是我们自定义的1个计算位移的工具类源码:
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;
}
}
分析1下,这个进程。
当我们在计算出切换到下1个View的id时,就能够得到切换的距离了,公式:要移动的距离 = 终究的位置 - 现在的位置;得到这个移动距离以后,拿到这个距离和初始位置,告知“工程师”――工具类MyScroller,这时候候可以开始计算了,初始化代码以下:
// 要移动的距离 = 终究的位置 - 现在的位置
int distanceX = currId * getWidth() - getScrollX();
// 设置运行的时间
myScroller.startScroll(getScrollX(), 0, distanceX, 0);
// 刷新视图
invalidate();
初始化完计算工具类以后,需要刷新当前视图了,调用invalidate()方法,这个方法会经过1系列连锁反应,事实上刷新视图是个很复杂的进程,这里不讲授了,1直直到触发computeScroll()方法,此时,我们需要重写父类的computeScroll()方法,在这个方法中,完成自己的1些操作:
/**
* invalidate();会致使这个方法的履行
*/
@Override
public void computeScroll() {
if (myScroller.computeOffset()) {
int newX = (int) myScroller.getCurrX();
System.out.println("newX::" + newX);
scrollTo(newX, 0);
invalidate();
}
}
在这个方法里,首先调用1下工具类计算位移的方法computeOffset()方法,该方法首先判断1下视图移动是不是完成,若完成返回false,若没有完成,先获得运动的时间间隔,如果当前运动的时间间隔在总时间间隔duration以内,那末通过时间间隔计算出这段时间间隔以后,视图实际移动到的位置,公式是:开始位置+总的距离/总的时间*本段移动时间间隔,如果当前运动的时间间隔超越了总的时间间隔,那末直接算出最后1次位置,公式:开始位置+移动距离。通过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);
}
}
}
另外,在这个例子程序中我自定义了1个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说明转动已完成。这是1个很重要的方法,通常放在View.computeScroll()中,用来判断是不是转动是不是结束。
Scroller的具体使用实践在我的前面博文中有用过,请移步Android自定义控件――侧滑菜单查看相干源码。
源码请在这里下载
生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠