android 仿viewpager滑动效果自定义升级版

来源:互联网 发布:哪里买正版windows 编辑:程序博客网 时间:2024/06/06 07:43

上篇讲了下仿viewpager竖直滑动的效果,那篇博客有的细节没说,那个是滑动到哪界面显示在那,我们一般是滑动的距离超过屏幕的一半就到下一个界面,而且就是实现了这个功能,还是会有一个问题,因为它是一瞬间完成的,所以从效果看起来体验不是很好,就像一个动画,如果在很短的时间内完成,你就看不出来有动画的效果,因此滑动看起来要有效果,得是在一定距离内有个时间在,这样看起来才有反弹的效果,今天先不用系统自带的Scroller类,我们自己实现下,然后再用Scroller,这样理解起来更深刻点,好的,现在开始写代码,

新建一个android项目:Customviewpager

先定义一个Customviewpager类继承ViewGroup,然后再布局文件中使用这个自定义的类

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="fill_parent"    android:layout_height="fill_parent"  >    <zhi.more.customviewpager.view.CustomViewPager        android:id="@+id/custom_view_pager"        android:layout_width="fill_parent"        android:layout_height="fill_parent"         /></RelativeLayout>
MainActivity.java

public class MainActivity extends Activity {private CustomViewPager custom_view_pager;private int[] ids = {R.drawable.a1,R.drawable.a2,R.drawable.a3,R.drawable.a4,R.drawable.a5,R.drawable.a6};private List<ImageView> imageViews;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);DisplayUtil.init(this);custom_view_pager = (CustomViewPager) findViewById(R.id.custom_view_pager);initData();}private void initData() {imageViews = new ArrayList<>();for(int i=0;i<ids.length;i++){ImageView imageView  = new ImageView(this);imageView.setBackgroundResource(ids[i]);imageViews.add(imageView);custom_view_pager.addView(imageView);}}}

我们主要讲下CustomViewPager的实现,我们都知道要自定义ViewGroup的话,onLayout()方法是必须实现,它的意思是说父view指定子view的位置,我们是仿viewpager效果,所以宽度是全屏的,高度也是,protected void onLayout(boolean changed, int l, int t, int r, int b) 这里面几个参数都是父view传递过来的,我们可以通过改变布局文件中的宽和高 你再打印出来onLayout()方法中的四个值会发现 变了,这就验证了onLayout()中的参数是父view传递过来的,如果要实现类似viewpager效果,那么子view怎么排放呢?也就是每个子view的位置该怎么设置呢?请看下面的图你就知道了:


通过上面的图我想应该知道了它的坐标点,只是我们屏幕有限超过屏幕外的坐标点我们看不到,但是拖动的时候就可以看的见了,那么在onLayout()方法中就是这么写了:

for(int i=0;i<getChildCount();i++){View childView = getChildAt(i);childView.layout(i*getWidth(), 0, (i+1)*getWidth(), getHeight());//这是水平方向滑动}
这只是帮我们把每个子view位置搞定了,但是还不能滑动啊,是的,android中view的滑动都是都是通过onTouchEvent()这个方法,它有个参数MotionEvent,这个类就封装了我们手在屏幕上一系列的操作,比如按下(down),移动(move),离开屏幕(up)等,如果就是想在onTouchEvent()方法中去实现当手滑动到超过屏幕一半就滑动到下一页这个逻辑,也能实现,但是比较麻烦,android提供了一个类GestureDetector,它封装了我们手在屏幕上更多的操作,什么双击屏幕,单击屏幕,滑动什么的,它的初始化也很简单,

detector = new GestureDetector(context, listener);第一个参数是上下文,第二个参数是一个接口,关于我们手在屏幕上操作的回调,他有很多方法,截图如下:


根据自己的逻辑看使用哪个方法,我们是要实现滑动的效果,所以我们只要实现onScroll()方法即可,

detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener(){/** * distanceX 在屏幕上要移动的距离 而不是坐标 */@Overridepublic boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {scrollBy((int)distanceX,0);return  true;}});
GestureDetector类是帮我们封装了滑动的操作,但是具体的滑动还是要我们去实现,在这里我们就要用到view提供的scrollBy方法了,它是实现在屏幕上滑动的距离,是叠加的,比如我们手一直往右边滑动,这个distanceX从3,4,7,9,它是累加的,这样我们手滑动到哪,view也就滑动到哪了,关于onScrollBy()返回为true,表示自己处理了这个事件,这个会涉及到事件传递,如果有什么不懂的,关于这方面,可以到网上找找博客看看,在这就不讲了,

那么我们onScroll()方法中的代码就是这样了

detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener(){/** * distanceX 在屏幕上要移动的距离 而不是坐标 */@Overridepublic boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {scrollBy((int)distanceX,0);return  true;}});
因为我们只在x轴上滑动,y轴就不用了,所以就为0,别忘记了记得把滑动事件,交给GestureDetector,所以在onTouchEvent()方法中:

public boolean onTouchEvent(MotionEvent event) {detector.onTouchEvent(event);return true;

到这里就实现了滑动的效果,虽然滑动效果是实现了,但是体验不好,不好的地方在于滑动到哪就显示到哪,因为一般的需求都是滑动超过屏幕的一半就显示下一个图,因此我们要实现这个效果,先简单分析下这个逻辑该怎么实现,其实就是要计算手指按下和离开屏幕之间的距离和屏幕宽度的一半进行对比,但是就是这样还会遇到一个问题啊,比如我现在在第三个界面,我向左滑动距离超过屏幕一半,那么这个时候就会移动到第二个界面,这个时候又用到了view给我们提供了一个方法,scrollTo(),这个方法是移动到指定的坐标点,再看下之前的图:


我手机是728*1280的分辨率,如果到第二个界面,它的坐标是(1440,0),y轴上的坐标我们是写死的,那么怎么计算x轴上的坐标呢?这个时候就要想到我们界面上有6个子view了,滑动到第二个界面的时候不就是下标(2)*getWidth(720)=1440,这样就ok了,因此我们要定义一个index变量记录当前滑动到第几个view了,OK,分析到此,代码就出来了:

@Overridepublic boolean onTouchEvent(MotionEvent event) {detector.onTouchEvent(event);switch (event.getAction()) {case MotionEvent.ACTION_DOWN:startX = event.getX();break;case MotionEvent.ACTION_MOVE:    break;case MotionEvent.ACTION_UP:float endX = event.getX();if((startX-endX)>getWidth()/2){//回到下一个页面index++;}else if((endX-startX)>getWidth()/2){//回到上一个界面index--;}moveTo(index);break;}return true;}

/** * 根据下标移动到指定的界面 * @param index */private void moveTo(int index) {if(index<0){index=0;}else if(index>getChildCount()-1){index = getChildCount()-1;}int x = index*getWidth();Log.e(TAG,"x="+x);scrollTo(x, 0);}
到目前为止我们实现了当滑动到超过屏幕的一半会回弹的问题,但是这样体验非常不好,因为我们是在瞬间完成的,这个也需要改动了,再分析下这个逻辑怎么实现:

比如从(0,0)坐标点,移动到(10,0)坐标点,是移动了10个像素点的距离,我可以人为的给它设置一个时间为500毫秒,那么它的速度就是10/500了,现在我把这从(0,0)到(10,0)这个过程分为10等分的话是不是看起来就不一样,体验更好点,这样速度就求出来了,速度怎么求,度过小学的都知道,那么如果我从(0,0)移动到(1,0)这个距离就为1了,但是我们滑动算的是坐标,那么就是开始位置0到滑动的距离1二者向加就是移动后的坐标了,好了,逻辑我们分析完了,现在动手写代码,我们可以新建一个工具类,专门维护这段逻辑

MyScroller.java



public class MyScroller {
private float startX;//x轴移动的的起始坐标
private float startY;
private int distanceX;//x轴要移动的距离
private int distanceY;
private long startTime;//手机的开机时间
private boolean isFinish;//是否移动完成
private long totalTime = 300;//移动这个动画所需要的总时间
private float currX;
public void startScroll(float startX ,float startY,int distanceX,int distanceY){
this.startX = startX;
this.startY = startY;
this.distanceX = distanceX;
this.distanceY = distanceY;
this.startTime = SystemClock.uptimeMillis();
this.isFinish = false;
}
/**
* 计算偏移量
* 移动一小段的时间
* 移动一小段的距离
* 移动一小段对应的坐标
* 移动的平均速度
* true 表示正在移动 false表示移动结束
*/
public boolean computeScrollOffSet(){
if(isFinish){//表示移动结束
return false;
}
long endTime = SystemClock.uptimeMillis();
long passTime = endTime-startTime;
if(passTime<totalTime){//表示还在移动
//还在移动
// float volecityX = distanceX / totalTime;
//距离 = 时间* 速度
//这一小段的距离
float distanceSmallX  =  passTime * distanceX / totalTime;
//移动一小段转换对应的坐标
currX = startX + distanceSmallX  ;
}else{//表示移动结束了
currX = startX + distanceX;
isFinish  = true;
}
return true;
}
public float getCurrX() {
return currX;
}
}
上面的代码就是根据刚才分析的逻辑完成的,现在就用一下:

我们之前是用scrollTo()完成的,现在我们这么调用:

贴一下主要的代码,因为上面都写过了:

/**
* 根据下标移动到指定的界面
* @param index
*/
private void moveTo(int index) {
if(index<0){
index=0;
}else if(index>getChildCount()-1){
index = getChildCount()-1;
}
int x = index*getWidth();
//要移动的总距离
int distanceX = x-getScrollX();
// scrollTo(x, 0);
myScroller.startScroll(getScrollX(), 0, distanceX, 0);
invalidate();
}

我们都知道调用invalidate();函数会导致onDraw()界面重绘,但是还由于一个方法也会因为调用了invalidate()会被每次都会调,就是computeScroll()方法了,

@Override
public void computeScroll() {
super.computeScroll();
if(myScroller.computeScrollOffSet()){
float currX = myScroller.getCurrX();
scrollTo((int)currX,0);
invalidate();
}
}

这就是每次移动到指定的坐标点,现在画一个图更好理解:



如果不使用我们写的MyScroll类,系统也给我们提供了一个类叫Scroller,和我们写的一样,使用也一样!ok到此结束


0 0
原创粉丝点击