关于 WheelView 组件的源码分析

我们都知道,在iOS里面有一种控件------滚筒控件(Wheel View),这通常用于设置时间/日期,非常方便,但Android SDK并没有提供类似的控件。这里介绍一下如何Android实现WheelView。

代码下载 : 

-- GitHub : https://github.com/han1202012/WheelViewDemo.git 

-- CSDN : http://download.csdn.net/detail/han1202012/8208997 ;

博客总结 :

博文内容 : 本文完整地分析了 WheelView 所有的源码, 包括其适配器类型, 两种回调接口(选中条目改变回调, 和开始结束滚动回调), 以及详细的分析了 WheelView 主题源码, 其中组件宽高测量, 手势监听器添加, 以及精准的绘图方法是主要目的, 花了将近1周时间, 感觉很值, 在这里分享给大家;

WheelView 使用方法 : 创建 WheelView 组件 --> 设置显示条目数 --> 设置循环 --> 设置适配器 --> 设置监听器 ;

自定义组件宽高获取策略 : MeasureSpec 最大模式 取 默认值 和 给定值中较小的那个, 未定义模式取默认值, 精准模式取 给定值;

自定义组件维护各种回调监听器策略 : 维护集合, 将监听器置于集合中, 回调接口时遍历集合元素, 回调每个元素的接口方法;

自定义组件手势监听器添加方法 : 创建手势监听器, 将手势监听器传入手势探测器, 在 onTouchEvent() 方法中回调手势监听器的 onTouchEvent()方法;

一. WheelView 简介

1. WheelView 效果

在 Android 中实现类似与 IOS 的 WheelView 控件 : 如图 

关于 WheelView 组件的源码分析

2. WheelView 使用流程

(1) 基本流程简介

获取组件 --> 设置显示条目数 --> 设置循环 --> 设置适配器 --> 设置条目改变监听器 --> 设置滚动监听器

a. 创建 WheelView 组件 : 使用 构造方法 或者 从布局文件获取 WheelView 组件;

b. 设置显示条目数 : 调用 WheelView 组件对象的 setVisibleItems 方法 设置;

c. 设置是否循环 : 设置 WheelView 是否循环, 调用 setCyclic() 方法设置;

d. 设置适配器 : 调用 WheelView 组件的 setAdapter() 方法设置;

e. 设置条目改变监听器 : 调用 WheelView 组件对象的 addChangingListener() 方法设置; 

f. 设置滚动监听器 : 调用 WheelView 组件对象的 addScrollingListener() 方法设置;

(2) 代码实例

a. 创建 WheelView 对象 : 

//创建 WheelView 组件
        final WheelView wheelLeft = new WheelView(context);


b. 设置 WheelView 显示条目数 : 

//设置 WheelView 组件最多显示 5 个元素
        wheelLeft.setVisibleItems(5);


c. 设置 WheelView 是否滚动循环 : 

//设置 WheelView 元素是否循环滚动
        wheelLeft.setCyclic(false);


d. 设置 WheelView 适配器 : 

//设置 WheelView 适配器
        wheelLeft.setAdapter(new ArrayWheelAdapter<String>(left));


e. 设置条目改变监听器 : 

//为左侧的 WheelView 设置条目改变监听器
        wheelLeft.addChangingListener(new OnWheelChangedListener() {
            @Override
            public void onChanged(WheelView wheel, int oldValue, int newValue) {
            	//设置右侧的 WheelView 的适配器
                wheelRight.setAdapter(new ArrayWheelAdapter<String>(right[newValue]));
                wheelRight.setCurrentItem(right[newValue].length / 2);
            }
        });


f. 设置滚动监听器 : 

wheelLeft.addScrollingListener(new OnWheelScrollListener() {
			
			@Override
			public void onScrollingStarted(WheelView wheel) {
				// TODO Auto-generated method stub
				
			}
			
			@Override
			public void onScrollingFinished(WheelView wheel) {
				// TODO Auto-generated method stub
				
			}
		});

二. WheelView  适配器 监听器 相关接口分析

1. 适配器 分析

这里定义了一个适配器接口, 以及两个适配器类, 一个用于任意类型的数据集适配, 一个用于数字适配;

适配器操作 : 在 WheelView.java 中通过 setAdapter(WheelAdapter adapter) 和 getAdapter() 方法设置 获取 适配器;

-- 适配器常用操作 : 在 WheelView 中定义了 getItem(), getItemsCount(), getMaxmiumLength() 方法获取 适配器的相关信息;

/**
     * 获取该 WheelView 的适配器
     * 
     * @return 
     * 		返回适配器
     */
    public WheelAdapter getAdapter() {
        return adapter;
    }

    /**
     * 设置适配器
     * 
     * @param adapter
     *            要设置的适配器
     */
    public void setAdapter(WheelAdapter adapter) {
        this.adapter = adapter;
        invalidateLayouts();
        invalidate();
    }

(1) 适配器接口 ( interface WheelAdapter )

适配器接口 : WheelAdapter;

-- 接口作用 : 该接口是所有适配器的接口, 适配器类都需要实现该接口;

接口抽象方法介绍 : 

-- getItemsCount() : 获取适配器数据集合中元素个数;

/**
     * 获取条目的个数
     * 
     * @return 
     * 		WheelView 的条目个数
     */
    public int getItemsCount();

-- getItem(int index) : 获取适配器集合的中指定索引元素;

/**
     * 根据索引位置获取 WheelView 的条目
     * 
     * @param index
     *            条目的索引
     * @return 
     * 		WheelView 上显示的条目的值
     */
    public String getItem(int index);

-- getMaximumLength() : 获取 WheelView 在界面上的显示宽度;

/**
     * 获取条目的最大长度. 用来定义 WheelView 的宽度. 如果返回 -1, 就会使用默认宽度
     * 
     * @return 
     * 		条目的最大宽度 或者 -1
     */
    public int getMaximumLength();

(2) 数组适配器 ( class ArrayWheelAdapter<T> implements WheelAdapter )

适配器作用 : 该适配器可以传入任何数据类型的数组, 可以是 字符串数组, 也可以是任何对象的数组, 传入的数组作为适配器的数据源;

成员变量分析 : 

-- 数据源 : 

/** 适配器的数据源 */
    private T items[];

-- WheelView 最大宽度 : 

/** WheelView 的宽度 */
    private int length;

构造方法分析 : 

-- ArrayWheelAdapter(T items[], int length) : 传入 T 类型 对象数组, 以及 WheelView 的宽度;

/**
     * 构造方法
     * 
     * @param items
     *            适配器数据源 集合 T 类型的数组
     * @param length
     *            适配器数据源 集合 T 数组长度
     */
    public ArrayWheelAdapter(T items[], int length) {
        this.items = items;
        this.length = length;
    }

-- ArrayWheelAdapter(T items[]) : 传入 T 类型对象数组, 宽度使用默认的宽度;

/**
     * 构造方法
     * 
     * @param items
     *            适配器数据源集合 T 类型数组
     */
    public ArrayWheelAdapter(T items[]) {
        this(items, DEFAULT_LENGTH);
    }

实现的父类方法分析 :

--  getItem(int index) : 根据索引获取数组中对应位置的对象的字符串类型;

@Override
    public String getItem(int index) {
    	//如果这个索引值合法, 就返回 item 数组对应的元素的字符串形式
        if (index >= 0 && index < items.length) {
            return items[index].toString();
        }
        return null;
    }

-- getItemsCount() : 获取数据集广大小, 直接返回数组大小;

@Override
    public int getItemsCount() {
    	//返回 item 数组的长度
        return items.length;
    }

-- getMaximumLength() : 获取 WheelView 的最大宽度;

@Override
    public int getMaximumLength() {
    	//返回 item 元素的宽度
        return length;
    }

(3) 数字适配器 ( class NumericWheelAdapter implements WheelAdapter )

NumericWheelAdapter 适配器作用 : 数字作为 WheelView 适配器的显示值;

成员变量分析 : 

-- 最小值 : WheelView 数值显示的最小值;

/** 设置的最小值 */
    private int minValue;

-- 最大值 : WheelView 数值显示的最大值;

/** 设置的最大值 */
    private int maxValue;


-- 格式化字符串 : 用于字符串的格式化;

/** 格式化字符串, 用于格式化 货币, 科学计数, 十六进制 等格式 */
    private String format;

构造方法分析 : 

-- NumericWheelAdapter() : 默认的构造方法, 使用默认的最大最小值;

/**
     * 默认的构造方法, 使用默认的最大最小值
     */
    public NumericWheelAdapter() {
        this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE);
    }

-- NumericWheelAdapter(int minValue, int maxValue) : 传入一个最大最小值;

/**
     * 构造方法
     * 
     * @param minValue
     *            最小值
     * @param maxValue
     *            最大值
     */
    public NumericWheelAdapter(int minValue, int maxValue) {
        this(minValue, maxValue, null);
    }

-- NumericWheelAdapter(int minValue, int maxValue, String format) : 传入最大最小值, 以及数字格式化方式;

/**
     * 构造方法
     * 
     * @param minValue
     *            最小值
     * @param maxValue
     *            最大值
     * @param format
     *            格式化字符串
     */
    public NumericWheelAdapter(int minValue, int maxValue, String format) {
        this.minValue = minValue;
        this.maxValue = maxValue;
        this.format = format;
    }

实现的父类方法 : 

-- 获取条目 : 如果需要格式化, 先进行格式化;

@Override
    public String getItem(int index) {
    	String result = "";
        if (index >= 0 && index < getItemsCount()) {
            int value = minValue + index;
            //如果 format 不为 null, 那么格式化字符串, 如果为 null, 直接返回数字
            if(format != null){
            	result = String.format(format, value);
            }else{
            	result = Integer.toString(value);
            }
            return result;
        }
        return null;
    }


-- 获取元素个数 : 

@Override
    public int getItemsCount() {
    	//返回数字总个数
        return maxValue - minValue + 1;
    }


-- 获取 WheelView 最大宽度 : 

@Override
    public int getMaximumLength() {
    	//获取 最大值 和 最小值 中的 较大的数字
        int max = Math.max(Math.abs(maxValue), Math.abs(minValue));
        //获取这个数字 的 字符串形式的 字符串长度
        int maxLen = Integer.toString(max).length();
        if (minValue < 0) {
            maxLen++;
        }
        return maxLen;
    }

2. 监听器相关接口

(1) 条目改变监听器 ( interface OnWheelChangedListener )

监听器作用 : 在 WheelView 条目改变的时候, 回调该监听器的接口方法, 执行条目改变对应的操作;

接口方法介绍 : 

-- onChanged(WheelView wheel, int oldValue, int newValue) : 传入 WheelView 组件对象, 以及 旧的 和 新的 条目值索引;

/**
     * 当前条目改变时回调该方法
     * 
     * @param wheel
     *            条目改变的 WheelView 对象
     * @param oldValue
     *            WheelView 旧的条目值
     * @param newValue
     *            WheelView 新的条目值
     */
    void onChanged(WheelView wheel, int oldValue, int newValue);

(2) 滚动监听器 ( interface OnWheelScrollListener )

滚动监听器作用 : 在 WheelView 滚动动作 开始 和 结束的时候回调对应的方法, 在对应方法中进行相应的操作;

接口方法介绍 : 

-- 开始滚动方法 : 在滚动开始的时候回调该方法;

/**
     * 在 WheelView 滚动开始的时候回调该接口
     * 
     * @param wheel
     *            开始滚动的 WheelView 对象
     */
    void onScrollingStarted(WheelView wheel);

-- 停止滚动方法 : 在滚动结束的时候回调该方法;

/**
     * 在 WheelView 滚动结束的时候回调该接口
     * 
     * @param wheel
     *            结束滚动的 WheelView 对象
     */
    void onScrollingFinished(WheelView wheel);

三. WheelView 解析

1. 触摸 点击 手势 动作操作控制组件 模块

(1) 创建手势监听器

手势监听器创建及对应方法 : 

-- onDown(MotionEvent e) : 在按下的时候回调该方法, e 参数是按下的事件;

-- onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) : 滚动的时候回调该方法, e1 滚动第一次按下事件, e2 当前滚动的触摸事件, X 上一次滚动到这一次滚动 x 轴距离, Y 上一次滚动到这一次滚动 y 轴距离;

-- onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) : 快速急冲滚动时回调的方法, e1 e2 与上面参数相同, velocityX 是手势在 x 轴的速度, velocityY 是手势在 y 轴的速度;

-- 代码示例 : 

/*
         * 手势监听器监听到 滚动操作后回调
         * 
         * 参数解析 : 
         * MotionEvent e1 : 触发滚动时第一次按下的事件
         * MotionEvent e2 : 触发当前滚动的移动事件
         * float distanceX : 自从上一次调用 该方法 到这一次 x 轴滚动的距离, 
         * 				注意不是 e1 到 e2 的距离, e1 到 e2 的距离是从开始滚动到现在的滚动距离
         * float distanceY : 自从上一次回调该方法到这一次 y 轴滚动的距离
         * 
         * 返回值 : 如果事件成功触发, 执行完了方法中的操作, 返回true, 否则返回 false 
         * (non-Javadoc)
         * @see android.view.GestureDetector.SimpleOnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent, float, float)
         */
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        	//开始滚动, 并回调滚动监听器集合中监听器的 开始滚动方法
            startScrolling();
            doScroll((int) -distanceY);
            return true;
        }

        /*
         * 当一个急冲手势发生后 回调该方法, 会计算出该手势在 x 轴 y 轴的速率
         * 
         * 参数解析 : 
         * -- MotionEvent e1 : 急冲动作的第一次触摸事件;
         * -- MotionEvent e2 : 急冲动作的移动发生的时候的触摸事件;
         * -- float velocityX : x 轴的速率
         * -- float velocityY : y 轴的速率
         * 
         * 返回值 : 如果执行完毕返回 true, 否则返回false, 这个就是自己定义的
         * 
         * (non-Javadoc)
         * @see android.view.GestureDetector.SimpleOnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent, float, float)
         */
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        	//计算上一次的 y 轴位置, 当前的条目高度 加上 剩余的 不够一行高度的那部分
            lastScrollY = currentItem * getItemHeight() + scrollingOffset;
            //如果可以循环最大值是无限大, 不能循环就是条目数的高度值
            int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();
            int minY = isCyclic ? -maxY : 0;
            /*
             * Scroll 开始根据一个急冲手势滚动, 滚动的距离与初速度有关
             * 参数介绍 : 
             * -- int startX : 开始时的 X轴位置
             * -- int startY : 开始时的 y轴位置
             * -- int velocityX : 急冲手势的 x 轴的初速度, 单位 px/s
             * -- int velocityY : 急冲手势的 y 轴的初速度, 单位 px/s
             * -- int minX : x 轴滚动的最小值
             * -- int maxX : x 轴滚动的最大值
             * -- int minY : y 轴滚动的最小值
             * -- int maxY : y 轴滚动的最大值
             */
            scroller.fling(0, lastScrollY, 0, (int) -velocityY / 2, 0, 0, minY, maxY);
            setNextMessage(MESSAGE_SCROLL);
            return true;
        }
    };

(2) 创建手势探测器

手势探测器创建 : 调用 其构造函数, 传入 上下文对象 和 手势监听器对象;

-- 禁止长按操作 : 调用 setIsLongpressEnabled(false) 方法, 禁止长按操作, 因为 长按操作会屏蔽滚动事件;

//创建一个手势处理
        gestureDetector = new GestureDetector(context, gestureListener);
        /*
         * 是否允许长按操作, 
         * 如果设置为 true 用户按下不松开, 会返回一个长按事件, 
         * 如果设置为 false, 按下不松开滑动的话 会收到滚动事件.
         */
        gestureDetector.setIsLongpressEnabled(false);

(3) 将手势探测器 与 组件结合

关联手势探测器 与 组件 : 在组件的 onTouchEvent(MotionEvent event) 方法中, 调用手势探测器的 gestureDetector.onTouchEvent(event) 方法即可;

/*
     * 继承自 View 的触摸事件, 当出现触摸事件的时候, 就会回调该方法
     * (non-Javadoc)
     * @see android.view.View#onTouchEvent(android.view.MotionEvent)
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    	//获取适配器
        WheelAdapter adapter = getAdapter();
        if (adapter == null) {
            return true;
        }

        /*
         * gestureDetector.onTouchEvent(event) : 分析给定的动作, 如果可用, 调用 手势检测器的 onTouchEvent 方法
         * -- 参数解析 : ev , 触摸事件
         * -- 返回值 : 如果手势监听器成功执行了该方法, 返回true, 如果执行出现意外 返回 false;
         */
        if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {
            justify();
        }
        return true;
    }