对于系统的ViewGroup我们已经是十分熟悉了,最常用的LinearLayout和RelativeLayout几乎是天天要打交道,下面我们就来看看,如何一步一步将其实现:

一、首先当然也是最通常的新建一个Android工程,然后在里面新建一个类继承自ViewGroup,这里我们把这个类叫做MyViewPager。

   实现继承ViewGroup之后,会发现,需要我们实现其中未实现的方法:第一个当然是两参的构造函数,用于xml布局文件生成对应的组件对象;第二个则是onLayout,这个方法我们应该也不陌生,在前几篇自定义view的博文中有提到过,onLayout方法实际作用是指定组件位置,在这里实际上是给ViewGroup的子View来确定位置。下面我们就需要来仔细考虑一下,我们的这个ViewGroup想要实现一个什么样的位置关系了:

加入说我们有6张图片想要显示,如果要实现viewpager效果的话,就需要一次显示一张图片,然后左右滑动的时候,可以显示前后的其他图片(如果前后还有图片的话);要实现这么一种效果,我们在onLayout方法里应该如何排列我们的子view呢(对于图片来说就是ImageView对象),看看下面这张图就会明白了:

如上图所示,在onLayout方法里,Android系统的坐标原点是位于手机屏幕的左上角顶点位置,x和y的正方向分别是向右和向下,对于ViewPager效果需要水平滑动的情况来说,我们的子View(ImageView)就应该要像上面图中所示的效果一样,在x-y坐标系中水平排列,这样我们手指水平滑动的时候才可以将左右的图片显示到手机屏幕上来。所以在这里,我们需要做的工作就是:

1、获取所有的子view。

2、将这些子view按照想显示的顺序,依次水平排列,每个子view的左上角与前后的子view的左上角的水平距离都相差一个手机屏幕的宽度,垂直方向上所有子view平齐。
 

用代码实现的话,如下:

/**
     * 对子View进行布局排列,确定子View的位置
     * 类似于iOS中的layoutSubView方法,iOS中的layoutSubViews一般到设置子View的frame时调用
     *
     * @param changed
     *            代表调用onLayout方法时,判断当前布局有没有发生改变,如果发生改变就为true,否则就为false
     * @param int l, int t, int r, int b
     *        代表当前View也就是myViewPager相对于它父View的位置,类似于ios中的坐标
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        /*
         * 获取子View的数量
         * getChildCount类似于iOS中的self.subViews.count
         */
        for (int i = 0; i < getChildCount(); i++) {
            View view  = getChildAt(i); // 获取下标为i的子View,类似于iOS中self.subViews[i];
            /*
             * Assign a size and position to a view and all of its descendants
              * This is the second phase of the layout mechanism. (The first is measuring).
              * In this phase, each parent calls layout on all of its children to position them.
              * This is typically done using the child measurements that were stored in the measure pass().
              * Derived classes should not override this method. Derived classes with children should override onLayout.
              * In that method, they should call layout on each of their children.
              * Parameters
              * l int: Left position, relative to parent
              * t int: Top position, relative to parent
              * r int: Right position, relative to parent
              * b int: Bottom position, relative to parent
             */
            int pageViewX = i * getWidth();
            int pageViewY = 0;
            int pageViewW = getWidth() * (i+1);
            int pageViewH = getHeight();

            Log.i("test", "当前View的宽度为:"+getWidth());
            Log.i("test", "当前View的高度为:"+getHeight());

            view.layout(pageViewX, pageViewY, pageViewW, pageViewH);
        }

    }

上面代码中view.layout方法中所对应的左、上、右、下四个参数分别对应于当前子view的左、上、右、下方向上的四条边,对应于父ViewGroup也就是我们自定义的ViewGroup的距离,所以排列好之后,效果并不是上图的效果,而是0号ImageView在手机屏幕显示的位置,而其他的ImageView依次排列在右边。

二、定义触摸事件,让我们可以使用手势来滑动我们之前使onLayout方法排列好的子view显示出来。

这里我们必不可少的要使用到onTouchEvent方法了,这个方法是触摸事件经由dispatchTouchEvent方法分发和onInterceptTouchEvent方法处理之后会首先将触摸事件传递到onTouchEvent方法中, 关于触摸事件的传递机制,将会在下一篇文章中进行介绍。按照我们之前的做法,就是在onTouchEvent中直接使用switch语句进行判断,在ACTION_DOWN,ACTION_MOVE和ACTION_UP三个状态中进行不同处理;在这里我们先使用Android提供的的一个类来处理:GestureDetectorGestureDetector可以用来接收一个事件,然后对事件的不同类型进行相应的处理:我们只要简单的使用gestureDetector.onTouchEvent(event);下面关键的地方在于创建这个GestureDetector对象的时候要实现的方法:

detector = new GestureDetector(context, new OnGestureListener() {

            @Override
            /**
             * 有一个手指抬起的时候回调
             */
            public boolean onSingleTapUp(MotionEvent e) {
                // TODO Auto-generated method stub
                return false;
            }

            @Override
            /**
             * 当有手指点击屏幕的时候
             */
            public void onShowPress(MotionEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            /**
             * 当有手指在屏幕上滑动的时候回调
             */
            public boolean onScroll(MotionEvent e1, MotionEvent e2,
                    float distanceX, float distanceY) {

                return false;
            }

            @Override
            /**
             * 当有手指在屏幕上长按的时候回调
             */
            public void onLongPress(MotionEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            /**
             * 快速滑动的时候回调
             */
            public boolean onFling(MotionEvent e1, MotionEvent e2,
                    float velocityX, float velocityY) {
                // TODO Auto-generated method stub
                return false;
            }

            @Override
            /**
             * 按下的时候回调的方法
             */
            public boolean onDown(MotionEvent e) {
                // TODO Auto-generated method stub
                return false;
            }
        });

不难看出GestureDetector对于事件的分类解析的方法有很多个。。。这里我们只关心滑动,所以只需要关注一下onScroll方法即可:public boolean onScroll(MotionEvent e1, MotionEvent e2,float distanceX, float distanceY) ,四个参数,e1表示最开始触发本次这一系列Event事件的那个ACTION_DOWN事件,e2表示触发本次Event事件的那个ACTION_MOVE事件,distanceX、distanceY分别表示从上一次调用onScroll调用到这一次onScroll调用在x和y方向上滑动的距离。这里需要稍微留意的是,distanceX、distanceY的正负并不是像之前ViewGroup里坐标显示的正负那样,而是向左滑动值distanceX为正,向右滑动值为distanceX负。

搞清楚参数的意思之后,下面要做的就是在onScroll让子View滑动起来~这里隆重介绍一个方法:public void scrollBy(int x, int y) ,在这里我们调用这个方法,将distanceX传进来,而第二个参数设为0(因为y方向上不需要滑动),即可达到我们的滑动目的了:scrollBy((int) distanceX, 0);在scrollBy方法内部,实际上是重写了scrollTo方法,scrollTo方法作用是让调用他的控件当前视图的基准点移动到某个坐标点,对于这里就是我们的ViewGroup,试想,如果基准点x坐标移动到负的屏幕宽度,那么现实的子view不就变成了1号了吗:

@Override
            /**
             * 当有手指在屏幕上滑动的时候回调
             */
            public boolean onScroll(MotionEvent e1, MotionEvent e2,
                    float distanceX, float distanceY) {

                // System.out.println("" + distanceX);
                // distanceX为正时,向左移动,为负时,向右移动
                // 移动屏幕的方法scrollBy,很重要,这个方法会调用onScrollChanged方法,并刷新视图
                /**
                 * dx表示x方向上移动的距离,dy表示y方向上移动的距离。往坐标轴正方向上移动的话,值就是正值;反之为负
                 */
                // scrollBy内部实际上是重写了scrollTo方法,scrollTo是将当前视图的基准点移动到某个坐标点
                scrollBy((int) distanceX, 0);

                return false;
            }

好了,至此,我们准备来看看效果;我们先在布局文件中将我们的自定义ViewGroup写出来:

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

然后在MainActivity中,将这个MyViewPager的对象创建出来myViewPager,有资源文件的图片,都放到drawable目录中,然后循环调用myViewPager.addview方法,给我们的ViewGroup添加子view:

package com.example.test;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import com.example.test.model.DJImage;
import com.example.test.view.MyViewPager;

public class MainActivity extends Activity {

    private MyViewPager vp_view;

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

    private void initUIView() {

        vp_view = (MyViewPager) findViewById(R.id.vp_view);

        DJImage djImage1 = new DJImage();
        djImage1.setDrawable(getResources().getDrawable(R.drawable.temp1));

        DJImage djImage2 = new DJImage();
        djImage2.setDrawable(getResources().getDrawable(R.drawable.temp2));

        DJImage djImage3 = new DJImage();
        djImage3.setDrawable(getResources().getDrawable(R.drawable.temp3));

        DJImage djImage4 = new DJImage();
        djImage4.setDrawable(getResources().getDrawable(R.drawable.temp4));

        List<DJImage> images = new ArrayList<DJImage>();
        images.add(djImage1);
        images.add(djImage2);
        images.add(djImage3);
        images.add(djImage4);

        vp_view.setImages(images);

    }

}

好了,至此,我们就可以先来看一看阶段效果啦~^_^:

看起来好像还不错,基本的滑动功能已经实现了,但是还是有一些问题存在:

1、在第一张图片和最后一张 图片显示的时候继续往外滑动,会显示空白的内容,并且停留

2、在抬起手指的时候,不会自动恢复到显示一个完整的图片状态

三、解决出现的两个问题:空白页面显示、抬起手指自动恢复选择显示完整图片

 

1、空白页面显示的问题

解决的方案就是使用一个整型值currentId来代表当前滑动到哪个位置了,最开始打开的时候位置为0,如果页面往右滑动,并且达到了滑动到下一个页面的条件,那么currentId就+1,如果是将要滑动到前一个页面,currentId就-1,当然currentId的值也是有范围的,那就是[0,getChildCount()-1],如果currentId<0了,我们就让其等于0,如果currentId>=getChildCount了,我们就让其等于getChildCount-1,然后滑动到对应currentId位置停下,这样就解决了

2、抬起手指的时候,让图片显示自动归位的问题

解决思路就是对手指抬起的时候距离最开始手指按下的时候滑动的距离,如果是往右或者往左距离大于屏幕距离的1/2,那么就往右或者往左滑动显示一个图片,如果不足1/2,则滑回之前显示的页面。这里涉及到按下和抬起两个动作,我们可以在MyViewPager的onTouchEvent方法中继续添加switch判断条件,来获取对应的值从而来判断滑动条件,主要就是要在onTouchEvent方法增加内容,我们来看看:

/**
     * down事件时的x坐标
     */
    private int firstX;

    private int currentId;//当前显示的图片的id,从0开始,最大值为getChildCount()-1

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        // 使用工具来解析触摸事件
        detector.onTouchEvent(event);

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            firstX = (int) event.getX();
            break;
        case MotionEvent.ACTION_MOVE:

            break;
        case MotionEvent.ACTION_UP:

            int tmpId = 0;
            System.out.println("currentId=" + currentId);
            // 手指向右滑动超过屏幕的1/2,当前的id应该-1
            if (event.getX() - firstX > getWidth() / 2) {
                tmpId = currentId - 1;
            } else if (firstX - event.getX() > getWidth() / 2) {
                tmpId = currentId + 1;
            } else {
                tmpId = currentId;
            }
            System.out.println("currentId=" + currentId);

            // 三目运算符的效率比if else效率要高很多
            int childCount = getChildCount();
            currentId = tmpId < 0 ? 0
                    : ((tmpId > childCount - 1) ? childCount - 1 : tmpId);

            scrollTo(currentId*getWidth(), 0);

            break;

        default:
            break;
        }

        // 消费掉本次事件
        return true;
    }

经过上面的修改,我们再来看看效果:

经过上面的改动,发现之前的两个问题都解决了,但是看着还是有点不爽是不是。。。松手之后一下就弹回去了,用户体验不太好啊。所以我们这里得添加一个松手之后的滑动延时机制,让其慢慢的多次滑动,最后滑动到我们要的位置。

四、松手滑动延时效果的实现:

 

对于这一块,Android实际上也是有API可以使用的,这里我们先来自己动手实现一下看看。

这里的核心思想,还是要调用scrollTo方法,只是要在手指抬起之后,先拿到要滑动的距离,然后在指定的时间内匀速滑动完这一段距离即可,所以我们需要多次调用scrollTo方法来在不同的时间点里面移动到不同的位置。我们自己写一个类DistanceProvider,来实现上面的功能。我们先拿到要移动的距离,然后将距离参数传入到DistanceProvider的开始滑动方法startScroll中。先将之前的scrollTo(currentId * getWidth(), 0);那一句注释掉,写一个方法:moveToDest();

    /**
     * 移动到目标位置
     */
    private void moveToDest() {
        // 起点的距离就是单个View的X坐标,终点的距离就是getScrollX
        int distance = currentId * getWidth() - getScrollX();
        mDistanceProvider.startScroll(getScrollX(), 0, distance, 0);
        invalidate(); // invalidate 会导致onDraw 和computeScroll方法被调用
    }

下面重点来关注一下DistanceProvider这个类要如何写,这里会涉及到一个知识点,那就是在invalidate方法中,除了会调用onDraw方法之外还会调用一个方法computeScroll,这个方法的父类里面没有实现,实际上就是提供给开发者来使用的,我们可以重写computeScroll方法,在里面判断自动滑动是否结束,如果没结束,拿到最新的滑动位置,并且再次刷新视图调用invalidate方法,那么这一次调用的invalidate方法又会调用computeScroll方法,直到自动滑动完成。

对于DistanceProviderl类,有如下实现:

package com.example.test.view;

import android.os.SystemClock;

public class DistanceProvider {

    private int startX;
    private int startY;
    private int distanceX;
    private int distanceY;

    private long startTime;

    private static final int SCROLL_DURATION = 300;

    private boolean isFinish;

    // 当前的X值
    private int currentX;
    // 当前的Y值
    private int currentY;

    /**
     * 开始滑动
     * @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 true: 代表运行结束 false 代表仍在运行
     */
    public boolean computeScrollOffset() {
        if (isFinish) {
            return isFinish;
        }

        // 计算单次滑动所需时间
        long passTime = SystemClock.uptimeMillis() - startTime;
        if (passTime < SCROLL_DURATION) {
            /*
             * 公式
             * distanceX           单位时间上移动的距离
             * ------------------- = ----------------------------- = 单位时间上移动的速度
             * totalTime              单位时间
             */

            currentX = (int) (startX + distanceX * passTime / SCROLL_DURATION);
            currentY = (int) (startY + distanceY * passTime / SCROLL_DURATION);
        } else {
            currentX = startX + distanceX;
            currentY = startY + distanceY;
            isFinish = true;
        }
        return false;
    }

    public int getCurrentX() {
        return currentX;
    }

}

对于自定义viewgroup中的computeScroll方法:

@Override
    public void computeScroll() {
        super.computeScroll();
        if (!mDistanceProvider.computeScrollOffset()) {
            int newX = mDistanceProvider.getCurrentX();
            scrollTo(newX, 0);
            // 再次刷新
            invalidate();
        }
    }

经过这一改动之后,我们就基本实现了ViewGroup的功能了。

android自定义控件(5)-实现ViewPager效果的更多相关文章

  1. Android重写HorizontalScrollView仿ViewPager效果

    Android提供的ViewPager类太复杂,有时候没有必要使用,所以重写一个HorizontalScrollView来实现类似的效果,也可以当做Gallery来用 思路很简单,就是重写onTouc ...

  2. Android自定义控件简单实现ratingbar效果

    先上图: 一开始让我自定义控件我是拒绝的,因为android很早以前就有一个控件ratingbar,但是设置样式的时候我发现把图片设置小一点就显示不全,一直找不到解办法!(可以设置系统的自带的小样式) ...

  3. Android重写HorizontalScrollView模仿ViewPager效果

    Android提供的ViewPager类太复杂,有时候没有必要使用,所以重写一个HorizontalScrollView来实现类似的效果,也可以当做Gallery来用 思路很简单,就是重写onTouc ...

  4. Android自定义控件实战——水流波动效果的实现WaveView

    转载请声明出处http://blog.csdn.net/zhongkejingwang/article/details/38556891 水流波动的波形都是三角波,曲线是正余弦曲线,但是Android ...

  5. Android自定义控件练手——波浪效果

    这一次要绘制出波浪效果,也是小白的我第一次还望轻喷.首先当然是展示效果图啦: 一.首先来说说实现思路. 想到波浪效果,当然我第一反应是用正余弦波来设计啦(也能通过贝塞尔曲线,这里我不提及这个方法但是在 ...

  6. Android自定义控件5--轮播图广告ViewPager基本实现

    本文地址:http://www.cnblogs.com/wuyudong/p/5918021.html,转载请注明源地址. 本文开始实现轮播图广告系列,这篇文章首先实现让图片滑动起来(ViewPage ...

  7. android自定义控件(4)-自定义水波纹效果

    一.实现单击出现水波纹单圈效果: 照例来说,还是一个自定义控件,观察这个效果,发现应该需要重写onTouchEvent和onDraw方法,通过在onTouchEvent中获取触摸的坐标,然后以这个坐标 ...

  8. Android 自定义View修炼-自定义HorizontalScrollView视图实现仿ViewPager效果

    开发过程中,需要达到 HorizontalScrollView和ViewPager的效果,于是直接重写了HorizontalScrollView来达到实现ViewPager的效果. 实际效果图如下: ...

  9. Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器

    1.概述 本篇博客的产生呢,是因为,群里的哥们暖暖给我发了个效果图,然后问我该如何实现顶部ViewPager指示器的字体变色,该效果图是这样的: 大概是今天头条的app,神奇的地方就在于,切换View ...

随机推荐

  1. Bzoj1189 [HNOI2007]紧急疏散evacuate

    1189: [HNOI2007]紧急疏散evacuate Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 2293  Solved: 715 Descr ...

  2. IDEA:Idea注册

    注册码查找: http://idea.lanyus.com ,然后点击 OK

  3. Zabbix邮件报警-->Script

    Version:3.0.1 邮件报警有两种media 1.Email zabbix发送报警邮件到指定smtp服务器(使用系统自带的sendmail,发送邮箱是zabbix服务器的本地邮箱账号) 再由s ...

  4. select 1 from dual 中的1表示的含义

    select 1 from dual   在这条sql语句中的1代表什么意思?查出来是个什么结果?   其实: select 1 from table; select anycol(目的表集合中的任意 ...

  5. [JavaEE] Entity中Lazy Load的属性序列化JSON时报错

    The server encountered an internal error that prevented it from fulfilling this request.org.springfr ...

  6. python数据类型和字符串(三)

    一.变量 变量声明变量 #!/usr/bin/env python age= gender1='male' gender2='female' 变量作用:保存状态(程序的运行本质是一系列状态的变化,变量 ...

  7. 【原】ajaxupload.js上传报错处理方法

    相信大家在工作中经常用到文件上传的操作,因为我是搞前端的,所以这里主要是介绍ajax在前端中的操作.代码我省略的比较多,直接拿js那里的 $.ajaxFileUpload({ url:'www.cod ...

  8. android webview里获取和设置cookie

    private class MyWebViewClient extends WebViewClient { public boolean shouldOverrideUrlLoading(WebVie ...

  9. Win7 配置Apache+PHP+Mysql环境

    第一.安装并配置APACHE(安装到D:\phpapache\Apache2.2) 1.安装时默认安装,Network Domain, Server Name 我填写我的计算机名,Administra ...

  10. 自然语言15.1_Part of Speech Tagging 词性标注

    QQ:231469242 欢迎喜欢nltk朋友交流 https://en.wikipedia.org/wiki/Part-of-speech_tagging In corpus linguistics ...