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

来源:互联网 发布:软件测试费用标准 编辑:程序博客网 时间:2024/05/21 06:24

效果图

这是我在一个项目中做的日期选择器,用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 0