自定义View 篇三 《手动打造ViewPage》

Posted 杨道龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义View 篇三 《手动打造ViewPage》相关的知识,希望对你有一定的参考价值。

有了之前自定义View的理论基础,有了ViewPage、事件分发机制、滑动冲突、Scroller使用等相关知识的铺垫,今天纯手动打造一款ViewPage。

1、完成基本的显示:

在MainActivity中:

public class MainActivity extends AppCompatActivity {

    private MyViewPage mViewPage;

    int[] imageIds = new int[]{
           R.drawable.pic_0,
           R.drawable.pic_1,
           R.drawable.pic_2,
           R.drawable.pic_3,
           R.drawable.pic_4,
           R.drawable.pic_5
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mViewPage = (MyViewPage) findViewById(R.id.myviewpage);

        //给自定义ViewPage添加孩子组件
        for (int i = 0; i < imageIds.length; i++) {
            ImageView imageView = new ImageView(this);
            imageView.setBackgroundResource(imageIds[i]);
            mViewPage.addView(imageView);
        }

    }
}
在MyViewPage中:

public class MyViewPage extends ViewGroup {

    public MyViewPage(Context context) {
        this(context,null);
    }

    public MyViewPage(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            //遍历所有孩子,手动安放每个孩子控件的位置
           getChildAt(i).layout(i*getWidth(),0,(i+1)*getWidth(),getHeight());
        }
    }
}

首先往自定义view里面添加了6张图片,在view的onLayout方法中,给每个孩子组件进行布局安放位置,因为位置都确定了,因而不用去进行测量和绘制也可以显示。

给每个孩子布局位置的算法如下:


此时运行:


2、实现可滑动效果

运行后,按照添加的顺序显示,第一张肯定显示的是第一个孩子控件对象的图片。但是此时是无法进行滑动的,我们使用手势识别器GestureDetector,让自定义的控件可以滑动:

private void init() {
    mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
        //手势识别器移动的监听回调。每次移动,都会回调该方法
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            //参数1:起点动作封装;参数2:终点动作封装;参数3x方向的移动距离;参数4y方向滑动距离
            scrollBy((int) distanceX,0);
            return true;
        }
    });
}
初始化手势识别器,重写手势滑动监听回调。每当滑动的时候,都会调用这里的方法,我们在这里直接调用ScrollBy()方法,更新当前控件(MyViewPage)内部孩子控件(图片)的位置。在这里需要知道 ScrollBy() Scroll To ()的区别:


scrollBy来移动一段相对的距离,属于温柔性质的。表示在原来坐标的基础上再改变参数大小的距离。对于x:大于零表示往左移,小于零表示往右移。对于y:大于零表示往上移,小于零表示往下移。
scrollBy(10, 0);从右往左移动10个像素
scrollBy(-10, 0);从左往右动10个像素

scrollTo就是把View移动屏幕的X和Y位置,属于强迫性质的。参数代表我一次性跳跃到该坐标位置。
我擦,中国语言真是博大精深啊~

   瞬间移动视图的内容: 利用View的scroll方法
    1). scrollBy(int x, int y) : 滑动指定的偏移量(从当前位置瞬间)
     x: x轴上的偏移量, x>0内容向左滑动, x<0内容向右滑动, x=0水平方向不滑动
     y: y轴上的偏移量, y>0内容向上滑动, y<0内容向下滑动, y=0垂直方向不滑动
    2). scrollTo(int x, int y) : 滑动到指定的偏移量(从当前位置瞬间)
     x: 目标位置x轴上的偏移量, x>0移动到原始位置的左侧, x<0移动到原始位置的右侧,x=0移动到水平原始位置,
     y: 目标位置y轴上的偏移量, y>0移动到原始位置的上侧, y<0移动到原始位置的下侧, y=0移动到垂直原始位置
 
View类的源代码如下所示,mScrollX记录的是当前View针对屏幕坐标在水平方向上的偏移量(getScrollX();),而mScrollY则是记录的时当前View针对屏幕在竖值方向上的偏移量(getScrollY();)。

    scrollTo就是把View移动到屏幕的X和Y位置,也就是绝对位置。而scrollBy其实就是调用的scrollTo,但是参数是当前mScrollX和mScrollY加上X和Y的位置,所以ScrollBy调用的是相对于mScrollX和mScrollY的位置。

手势识别器要想使用,需要把touch事件委托给手势识别器来处理:

@Override
public boolean onTouchEvent(MotionEvent event) {
    //事件委托交手势识别器
    mGestureDetector.onTouchEvent(event);
    return true;
}
运行程序效果:


可以看到,此时我们实现了滑动效果。

3、滑动到某个位置后自动到合适位置下标停留

此时的滑动显然是不可行的,我们需要跟ViewPage那样,滑动小于屏幕一半时,跳转到当前页面,滑动距离大于一半时,跳转到下一页。让我们来实现利逻辑吧:

switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        break;
    case MotionEvent.ACTION_MOVE:
        break;
    case MotionEvent.ACTION_UP:
        //松开手的时候,根据当前位置,来确定下一个页面
        int scrollX = getScrollX();
        //当前页的索引值
        int pageIndex = scrollX / getWidth();
        int offset = scrollX % getWidth();
        if(offset > getWidth()/2){
            pageIndex++;
        }
        //处理越界问题
        if(pageIndex > getChildCount()-1){
            pageIndex = getChildCount()-1;
        }
         goCurrentPage(pageIndex);
        break;
    default:
        break;
    }

我们重写了touch事件,因为监听到底是否在页面一半位置,是手指离开屏幕时候决定的,因而逻辑写在UP事件里面即可。相信这个小算法难不倒你。

当前的pageIndex就是对应的页码,而且处理了越界问题。最后再生成一个方法,专门用于跳转页面功能。具体代码如下:

/**
 * 根据当前
 * @param pageIndex
 *      当前的page页面
 */
private void goCurrentPage(int pageIndex) {

    scrollTo(pageIndex*getWidth(),0);
}
是的,只需要一行代码就可以了!运行起来看看效果吧:


此时跟原生的ViewPgae挺像了,但是还是稍有区别的,即滑动跳转很是生硬。这是由于使用了ScrollTo()进行了强制跳转的缘故。为了与ViewPage更贴近,我们使用系统提供的类:Scoller来解决生硬问题。

Scoller的具体用法可以参考博客:Android Scroller完全解析

4、回弹过程解决办法

修改ScrollTo(),被Scoller取代:

private void goCurrentPage(int pageIndex) {
    //scrollTo(pageIndex*getWidth(),0);

    int dx = pageIndex*getWidth() - getScrollX();

    Log.e("YDL",dx+"-------");

    //参数1x的起始值;参数2y的起始值;参数3x的偏移量;参数4y的偏移量
    //对于参数3dx>0往左移动;dx<0往右移动
    mScroller.startScroll(getScrollX(),0,dx,0,Math.abs(dx));//dx绝对值作为时间值,按比例可以实现了匀速移动
    //使用Scroller必须重新刷新界面,不刷新的话不会滑动
    invalidate();
}
在这里 需要借助Scroller来完成后续的滚动操作。接下来我们就调用startScroll()方法来初始化滚动数据并刷新界面。startScroll()方法接收四个参数,第一个参数是滚动开始时X的坐标,第二个参数是滚动开始时Y的坐标,第三个参数是横向滚动的距离,正值表示向左滚动,第四个参数是纵向滚动的距离,正值表示向上滚动。紧接着调用invalidate()方法来刷新界面。

在这里比较难理解的可能是dx值的计算方式。我也通过两张图片来分析里面的算法:

图一:移动距离超过半个屏幕,应该执行跳转下一页的功能。因此dx=i*getWidth()-getScrollX();可以自行测试。


图二:移动的距离小于屏幕一半,执行跳转上一页的功能:


现在前两步都已经完成了,最后我们还需要进行第三步操作,即重写computeScroll()方法

//Scroller使用调用invalidate();后,会同步调用computeScroll()方法
@Override
public void computeScroll() {
    if(mScroller.computeScrollOffset()){
        int currX = mScroller.getCurrX();
        Log.e("YDL",currX+"");
        scrollTo(mScroller.getCurrX(),0);
        //也要刷新界面
        invalidate();
    }
}

并在其内部完成平滑滚动的逻辑 。在整个后续的平滑滚动过程中,computeScroll()方法是会一直被调用的,因此我们需要不断调用Scroller的computeScrollOffset()方法来进行判断滚动操作是否已经完成了,如果还没完成的话,那就继续调用scrollTo()方法,并把Scroller的currenX和currentY坐标传入,然后刷新界面从而完成平滑滚动的操作。
那么我们运行程序看看效果吧:


可以看到,效果跟系统自带的ViewPage几乎一模一样了。那么最后,再分析一下Scroller进行滚动的原理吧。


平滑移动视图的内容: 利用Scoller和View的scroll方法
    1). Scoller是实现View平滑移动的帮助类, 它本身并不能实现对View的移动
    2). 平滑移动的基本原理: 将整个从起始位置到结束位置的移动分解成多个小的距离, 循环调用scrollTo()实现平滑移动
    3). 相关API:
     a. Scoller类:
       -->Scoller(Context context) : 创建对象的构造方法
       -->startScroll(int startX, int startY, int dx, int dy, int duration) : 开始平滑移动视图(这个方法本身不会产生滑动)
         startX : 起始位置的X偏移量
         startY : 起始位置的Y偏移量
         dx: 滑动多大的X偏移量(如果是0,X方向不会滑动)
         dy: 滑动多大的Y偏移量(如果是0,Y方向不会滑动)
         duration : 整个过程持续的时间(ms)
       -->startScroll(int startX, int startY, int dx, int dy): 开始平滑移动视图(时间为250ms)
       -->boolean computeScrollOffset() : 计算当前移动的偏移量, 并将其保存到Scoller对象中, 如果滑动还没有完成返回true
       -->int getCurrX() : 得到计算出的X偏移量
       -->int getCurrY() : 得到计算出的Y偏移量
     b. View类
       -->invalidate() : 强制重绘, 导致draw()-->computeScroll()
         在scoller.startScroll()后必须执行此方法
       -->computeScroll() : 需要重写此方法, 用于计算移动, 此方法在draw()中调用
         调用scoller计算移动偏移量
         调用view对象scrollTo()到计算出的偏移量
         调用View对象invalidate()强制重绘, 导致computeScroll()再次执行


我们在上面的代码中可以看到当我们手指不段移动屏幕时,就会调用scrollBy来移动一段相对的距离。而当我们手指松开后,会调用mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration);来产生一段动画来移动到相应的页面,在这个过程中系统会不断调用computeScroll(),我们再使用scrollTo来把View移动到当前Scroller所在的绝对位置。


到目前为止,该自定义ViewPage控件算是讲完了。

源码下载地址


打开微信扫描下方二维码查看更多安卓文章:


打开微信搜索公众号    android程序员开发指南   或者手机扫描下方二维码 在公众号阅读更多Android文章。

微信公众号图片:




以上是关于自定义View 篇三 《手动打造ViewPage》的主要内容,如果未能解决你的问题,请参考以下文章

android自定义view,打造绚丽的验证码

手把手教你打造一个心电图效果View Android自定义View

android 中怎么关掉viewpage的滑动效果

Android自定义ViewGroup(四打造自己的布局容器)

Android自定义ViewGroup打造各种风格的SlidingMenu

Android打造万能自定义阴影控件