Android 打造自己的滚动选择器ScrollSelector

效果图

这是我在一个项目中做的日期选择器,用PopupWindow+自定义View(ScrollSelector)来实现的,其中最关键的是三个滚动选择器(年月日),是用我自定义的View:ScrollSelector来实现的。本来网上已经有别人做的类似的控件的了,不过我想要自己做一个。


上效果图

效果图


工程目录

工程目录

我们要关注的就只有这三个文件


MainActivity.java

<span style="font-size:18px;">import android.app.Activity;
import android.os.Bundle;

import java.util.ArrayList;

public class MainActivity extends Activity {

    private ScrollSelector scrollSelector;  //滚动选择器

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

        //获取滚动选择器控件
        scrollSelector = (ScrollSelector) findViewById(R.id.scrollSelector);

        //初始项列表
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 20; i++){
            list.add("第" + i + "项");
        }

        //设置滚动选择器的项列表
        scrollSelector.setItemContents(list);
    }
 }</span>

这里生成20个测试数据,传给ScrollSelector


activity_main.xml

<span style="font-size:18px;"><?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">

    <com.ffpy.demo.ScrollSelector
        android:id="@+id/scrollSelector"
        android:layout_width="200dp"
        android:layout_height="300dp"
        android:background="@android:color/darker_gray"/>

</LinearLayout></span>
这里把ScrollSelector的背景色设置为灰色,与布局的背景色不同,是为了能够将控件和布局背景区分开来,方便测试


ScrollSelector.java

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;

import java.util.ArrayList;

/**
 * Created by Administrator on 2016/9/7.
 * 滚动选择器
 */
public class ScrollSelector extends View {

    /**
     * 获取选中项
     */
    public int getSelectedIndex(){
        return (int) (-offsetY + 0.5);
    }

    /**
     * 设置选中项
     */
    public void setSelectedIndex(int pos){
        if (pos < 0 || pos >= contents.size()) return;
        offsetY = -pos + 1;
    }

    /**
     * 设置项列表的内容
     */
    public void setItemContents(ArrayList<String> list){
        /*当前选中项为原本列表项的最后一项时,如果重新指定的列表项的比原本的列表项的小
          则会让当前的选中项为空,所以需要重新指定选中项*/
        if (getSelectedIndex() >= list.size()){
            setSelectedIndex(list.size() - 1);
        }

        contents = list;
        invalidate();
    }

    /**
     * 设置显示的项数
     */
    public void setShowItemNum(int num){
        showItemNum = num;
        offsetY = (showItemNum - 1) / 2;    //设置默认项
    }

    /**
     * 设置分割线的颜色
     */
    public void setDividerColor(int dividerColor) {
        this.dividerColor = dividerColor;
    }

    /**
     * 设置选中状态字体的颜色
     */
    public void setTextSelectorColor(int textSelectorColor) {
        this.textSelectorColor = textSelectorColor;
    }

    /**
     * 设置正常状态字体的颜色
     */
    public void setTextNormalColor(int textNormalColor) {
        this.textNormalColor = textNormalColor;
    }

    private final int DIVIDER_WIDTH = 2;        //分割线的宽度
    private final int DEFAULT_TEXTSIZE = 50;    //默认字体大小
    private final int SLEEP_TIME = 1000 / 60;   //动画的延时时间,每秒大约80帧
    private final int WHAT_INVALIDATE = 0;      //重新绘制

    private int showItemNum = 3;                    //显示的项数
    private int dividerY;                           //绘制分隔线的y坐标
    private int itemHeight;                         //每一项所占的高度
    private int dividerColor = 0xFF8A8A8A;          //分割线的颜色
    private int textSelectorColor = 0xFFFF0000;     //选中状态文字的颜色
    private int textNormalColor = 0xFF000000;       //正常状态文字的颜色
    private int marqueeX;                           //跑马灯的x坐标偏移
    private int marqueeWidth;                       //跑马灯的宽度
    private int borderWhenDown;                     //按下时的边界状态
    private float offsetY;                          //项偏移的y坐标
    private boolean isPress;                        //手指是否是按下状态
    private boolean isFirst = true;                 //是否是首次绘制
    private boolean isSkiping;                      //是否正在执行跳转
    private boolean isStopSkiping;                  //是否要停止跳转
    private boolean isHoming;                       //是否正在执行归位
    private ArrayList<String> contents;             //项的内容
    private Paint mPaint;                           //画笔
    private GestureDetector mDetector;              //手势
    private Handler mHandler;                       //异步处理
    private RollThread rollThread;                  //滚动线程
    private MarqueeThread marqueeThread;            //跑马灯线程

    public ScrollSelector(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);                          //实例化画笔
        mPaint.setTextSize(DEFAULT_TEXTSIZE);                               //设置字体大小
        mPaint.setStrokeWidth(DIVIDER_WIDTH);                               //设置线条的宽度
        mDetector = new GestureDetector(context, new MyGestureListener());  //实例化手势
        contents = new ArrayList<>();                                       //初始化列表项的内容,防止出现空指针错误
        mHandler = new Handler(){                                           //实例化Handler
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                switch (msg.what){
                    //重新绘制
                    case WHAT_INVALIDATE:
                        invalidate();
                        break;
                }
            }
        };
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //计算每一项的高度
        itemHeight = h / showItemNum;

        //计算分割线的y坐标
        dividerY = itemHeight * ((showItemNum - 1) / 2);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //绘制分割线
        mPaint.setColor(dividerColor);
        canvas.drawLine(0, dividerY, getWidth(), dividerY, mPaint);
        canvas.drawLine(0, dividerY + itemHeight, getWidth(), dividerY + itemHeight, mPaint);

        //边界限制
        borderLimit();

        //绘制项
        for (int i = 0; i < showItemNum + 1; i++){
            //获取要绘制的项的序号
            int index = (int) -offsetY + i - (showItemNum - 1) / 2;
            if (index >= contents.size()) break;
            if (index < 0) continue;

            //获取字符串的宽高
            String item = contents.get(index);
            Rect bound = new Rect();
            mPaint.getTextBounds(item, 0, item.length(), bound);

            //绘制字符串
            int x = bound.width() > getWidth() ? 0 :(getWidth() - bound.width()) / 2;   //绘制文本的x坐标
            int y = (int) (itemHeight * i + (offsetY - (int) offsetY) * itemHeight);    //绘制文本的y坐标
            y += (itemHeight + bound.height()) / 2;                                     //绘制文本的基线偏移量

            if (getSelectedIndex() == index) {
                mPaint.setColor(textSelectorColor); //选中状态的字体颜色
                //判断是否需要跑马灯
                if (bound.width() > getWidth()) {
                    marqueeWidth = bound.width();
                    x = marqueeX;
                    if (isFirst){
                        marquee();
                    }
                }else{
                    marqueeWidth = 0;
                }
            }else {
                mPaint.setColor(textNormalColor);   //正常状态的字体颜色
            }

            canvas.drawText(item, x, y, mPaint);

            if (isFirst) isFirst = false;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //手指按下
        if (event.getAction() == MotionEvent.ACTION_DOWN){
            isPress = true;
            borderWhenDown = borderLimit();
            marqueeX = 0;
            if (isSkiping) isStopSkiping = true;
        }
        //手指抬起
        if (event.getAction() == MotionEvent.ACTION_UP){
            isPress = false;
            if (!isSkiping) {
                homing();
            }
            marquee();
        }
        //手势判断
        mDetector.onTouchEvent(event);

        return true;
    }

    /**
     * 归位
     */
    private void homing(){
        if (isHoming) return;
        isHoming = true;
        new HomingThread().start();
    }

    /**
     * 滚动
     */
    private void roll(float speed){
        if (rollThread != null && rollThread.isAlive()) return;

        rollThread = new RollThread(speed);
        rollThread.start();
    }

    /**
     * 跳转
     * @param dir true为跳转到顶部,false为跳转到底部
     */
    private void skip(boolean dir){
        if (isSkiping) return;
        isSkiping = true;
        Log.e("tag", "skip");
        new SkipThread(dir).start();
    }

    /**
     * 跑马灯显示
     */
    private void marquee(){
        if (marqueeThread != null && marqueeThread.isAlive()) return;

        marqueeThread = new MarqueeThread();
        marqueeThread.start();
    }

    /**
     * 边界限制
     * @return -1为在顶部,1为在底部,0为不在边界
     */
    private int borderLimit(){
        if (offsetY >= 0) {                          //顶部边界
            offsetY = 0;
            return -1;
        }
        else if (offsetY <= -contents.size() + 1){   //底部边界
            offsetY = -contents.size() + 1;
            return 1;
        }
        return 0;
    }

    /**
     * 手势事件
     */
    private class MyGestureListener extends GestureDetector.SimpleOnGestureListener{

        /**
         * 滑动
         */
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            int border = borderLimit();
            if (borderWhenDown == -1 && border == -1 && distanceY < 0) {         //跳转到底部
                skip(false);
            } else if (borderWhenDown == 1 && border == 1 && distanceY > 0) {    //跳转到顶部
                skip(true);
            }else if (!isSkiping) {
                offsetY -= distanceY / itemHeight;      //偏移量
                invalidate();
            }

            return false;
        }

        /**
         * 滚动
         */
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            float speed = velocityY / itemHeight / 20;
            if (Math.abs(speed) > 0.5) {
                roll(speed);
            }
            return super.onFling(e1, e2, velocityX, velocityY);
        }
    }

    /**
     * 自动返回中间位置的(归位)线程
     */
    private class HomingThread extends Thread{

        private final float MOVE_DISTANCE = 0.05f;   //每一帧的移动距离(itemHeight的比例)

        @Override
        public void run() {
            super.run();

            float dy = 0;
            while(!isPress){    //手指按下就停止归位
                //取小数部分
                float decimal = Math.abs(offsetY - (int) offsetY);
                //大概达到中间位置
                if (decimal > -MOVE_DISTANCE * 1.1 && decimal < MOVE_DISTANCE * 1.1) break;
                //移动量
                dy = decimal < 0.5 ? MOVE_DISTANCE : -MOVE_DISTANCE;
                //防止超过位置
                if ((int) offsetY != (int) (offsetY + dy)) break;

                offsetY += dy;

                try{
                    Thread.sleep(SLEEP_TIME);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                //重新绘制
                mHandler.sendEmptyMessage(WHAT_INVALIDATE);
            }
            //取整
            if (!isPress) {
                offsetY = (int) offsetY;
                if (dy < 0) {   //误差校正
                    offsetY--;
                }
                mHandler.sendEmptyMessage(WHAT_INVALIDATE);
            }
            isHoming = false;
        }
    }

    /**
     * 滚动的线程
     */
    private class RollThread extends Thread{

        private final float DAMPING = 0.1f;    //速度的衰减,即每一帧之后的衰减量

        private float speed;        //滚动的速度,即每一帧移动的距离

        public RollThread(float speed){
            this.speed = speed;
        }

        @Override
        public void run() {
            super.run();

            boolean dir = speed > 0;   //滚动方向,true为向上,false为向下
            while (!isPress){
                offsetY += speed;
                //显示越界
                if (borderLimit() != 0) {
                    mHandler.sendEmptyMessage(WHAT_INVALIDATE);
                    break;
                }
                //速度衰减
                speed += (dir ? -DAMPING : DAMPING);
                //速度越界
                if ((dir && speed < 0) || (!dir && speed > 0)) break;

                try {
                    Thread.sleep(SLEEP_TIME);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                //重新绘制
                mHandler.sendEmptyMessage(WHAT_INVALIDATE);
            }
            //滚动完后归位
            if (!isPress) {
                homing();
            }
        }
    }

    /**
     * 顶部和底部的跳转
     */
    private class SkipThread extends Thread{

        private final float SKIP_TIME = 1000;    //跳转时间,1秒

        private boolean dir;    //true为跳转到顶部,false为跳转到底部

        public SkipThread(boolean dir){
            this.dir = dir;
        }

        @Override
        public void run() {
            super.run();

            float framesNum = SKIP_TIME / SLEEP_TIME;     //总帧数
            float speed = (contents.size()) / framesNum;    //每帧移动的距离
            if (!dir) speed *= -1;

            while (!isStopSkiping && getSelectedIndex() != (dir ? 0 : contents.size() - 1)){
                offsetY += speed;

                try {
                    Thread.sleep(SLEEP_TIME);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }

                mHandler.sendEmptyMessage(WHAT_INVALIDATE);
            }
            if (!isPress){
                homing();
            }
            isSkiping = false;
            isStopSkiping = false;
        }
    }

    /**
     * 过长文字跑马灯显示的线程
     */
    private class MarqueeThread extends Thread {

        private final int moveDistance = 3;    //每一帧的移动距离

        @Override
        public void run() {
            super.run();

            while (!isPress && marqueeWidth != 0){
                marqueeX -= moveDistance;

                if (marqueeX < -marqueeWidth) marqueeX = getWidth();

                try {
                    Thread.sleep(SLEEP_TIME);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }

                mHandler.sendEmptyMessage(WHAT_INVALIDATE);
            }
        }
    }
}



下面给出一些关键的计算过程


分割线位置的计算

在onSizeChanged方法中

//计算分割线的y坐标
dividerY = itemHeight * ((showItemNum - 1) / 2);</span>

分割线有两条,一条在上面一条在下面,很容易就知道分割线的x坐标为从0到getWidth(),关键是分割线y坐标的计算。

用dividerY来存储上面那条分割线的y坐标,下面那条分割线的y坐标就是dividerY + itemHeight,所以只需要计算出dividerY的值就可以了

分割线位置的计算

可以看的出,dividerY = itemHeight * ((showItemNum - 1) / 2)


要绘制的列表项的个数

在onDraw方法中,看这一句

for (int i = 0; i < showItemNum + 1; i++){

我们要显示shwoItemNum个项,但是我们要绘制showItemNum+1项,为什么呢?看图

以showItemItem=3时为例:

这是平常状态,要显示3项


这是拖动状态,要显示4项


那为什么不把这两种状态分开来呢?因为没必要,拖动状态则要频繁的绘制,而平常状态只要绘制一次就可以了,多出的那一项因为超出View的高度所以并不会显示出来,也不会增加多少负担。而且将它们分开处理还会增加代码的复杂性,更容易出错。


拖动绘制的计算

这里用到了手势,如果对手势不了解的可以看这篇文章http://www.runoob.com/w3cnote/android-tutorial-gestures.html

在onScroll方法中

offsetY -= distanceY / itemHeight;  //偏移量</span>
distanceY是手指在屏幕上滑动的像素,取它相对于itemHeight的比例,加到offsetY。这里用“-=”是当手指向上滑动时,distanceY的值是正数,而绘制的项y坐标是向上偏移的,所以offsetY的值是变小的。


offsetY的整数部分就是当前的选中项,小数部分就是绘制的y坐标偏移(相对于itemHeight)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值