自定义滑动选择View

00CA6045E4F124CC21BF813006E740A8.gif

代码:

package com.example.gyfpickerview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller;

import java.util.ArrayList;

/**
 * Created by 龚勇峰 on 2017/6/22.
 */

public class GyfPickerView extends View {
    //文字大小
    private int textSize;
    //文字顔色,默认color.blcak
    private int textColor;
    //文字之间的间隔;默认10dp
    private int textPading;
    //文字放大比例,默认2.0f
    private float textMaxScale;
    //文本最小的alpha
    private float textMinAlpha;
    //时候循环,默认循环
    private boolean isRecycleMode;
    //文本对象,主要有 top bottom asent,desent,四个属性
    private Paint.FontMetrics fm;
    //显示的行数,默认是3
    private int maxShowNum;

    private Scroller scroller;
    //速率追踪器
    private VelocityTracker velocityTracker;
    //数据
    private ArrayList<String> dataList = new ArrayList<>();
    //中间x坐标
    private int cx;
    //中间y坐标
    private int cy;
    //文字的最大宽度
    private float textMaxwidth;
    //文字的高度
    private int textHeight;
    //实际内容的宽度
    private int contentWidth;
    //实际内容的高度
    private int contentHeight;


    //按下时y的坐标
    private float downY;
    //本次滑动Y坐标的偏移值
    private float offsetY;
    //在fling之前的offsetY
    private float oldOffsetY;
    //当前的选中项
    private int curIndex;
    //滚动的偏移项
    private int offsetIndex;
    //回弹距离
    private float bonceDistance;
    //是否处于滑动状态
    private boolean isSilding = false;
    private TextPaint textPaint;

    private int minNumVelocity;
    private int maxNumVelocity;
    private int scaleTouchSlop;

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

    public GyfPickerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GyfPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GyfPickerView);
        textSize = a.getDimensionPixelSize(R.styleable.GyfPickerView_gyfTextSize,
                getDimension(TypedValue.COMPLEX_UNIT_SP, 16, context));
        textColor = a.getColor(R.styleable.GyfPickerView_gyfTextColor, Color.BLACK);
        textPading = a.getDimensionPixelSize(R.styleable.GyfPickerView_gyfTextPading, getDimension(
                TypedValue.COMPLEX_UNIT_DIP, 20, context));
        textMaxScale = a.getFloat(R.styleable.GyfPickerView_gfyTextMaxScale, 2.0f);
        textMinAlpha = a.getFloat(R.styleable.GyfPickerView_gyfTextMinAlpha, 0.4f);
        isRecycleMode = a.getBoolean(R.styleable.GyfPickerView_gyfRecycleMode, true);
        maxShowNum = a.getInteger(R.styleable.GyfPickerView_gyfMaxShowNum, 3);
        a.recycle();

        textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(textColor);
        textPaint.setTextSize(textSize);
        fm = textPaint.getFontMetrics();

        scroller = new Scroller(context);
        velocityTracker = VelocityTracker.obtain();

        minNumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
        maxNumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
        scaleTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量宽度
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        contentWidth = (int) (textMaxwidth * textMaxScale + getPaddingLeft() + getPaddingRight());
        if (mode != MeasureSpec.EXACTLY) {
            //处理warp_content的情况
            width = contentWidth;

        }
        //测量高度
        mode = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        textHeight = (int) (fm.bottom - fm.top);
        contentHeight = textHeight * maxShowNum + textPading * maxShowNum;
        if (mode != MeasureSpec.EXACTLY) {
            height = contentHeight + getPaddingTop() + getPaddingBottom();
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //确定中心点坐标
        cx = w / 2;
        cy = h / 2;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        addVelocityTracker(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!scroller.isFinished()) {
                    scroller.forceFinished(true);
                    finishScroll();
                }
                downY = event.getY();
                break;

            case MotionEvent.ACTION_MOVE:
                offsetY = event.getY() - downY;
                if (isSilding || Math.abs(offsetY) > scaleTouchSlop) {
                    isSilding = true;
                    reDraw();
                }
                break;

            case MotionEvent.ACTION_UP:
                int scrollYVelocity = 2 * getScrollYVelocity() / 3;
                if (Math.abs(scrollYVelocity) > minNumVelocity) {
                    oldOffsetY = offsetY;
                    scroller.fling(0, 0, 0, scrollYVelocity, 0, 0, -Integer.MAX_VALUE, Integer.MAX_VALUE);
                    invalidate();
                } else {
                    finishScroll();
                }

                // 没有滑动,则判断点击事件
                if (!isSilding) {
                    if (downY < contentHeight / 3)
                        moveBy(-1);
                    else if (downY > 2 * contentHeight / 3)
                        moveBy(1);
                }

                isSilding = false;
                recycleVelocityTacvker();
                break;
        }
        return true;
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (dataList.isEmpty())
            return;
        //规定的绘制区域,我们要绘制maxShowCount+2个,但只显示maxShowCount
        canvas.clipRect(cx - contentWidth / 2, cy - contentHeight / 2,
                cx + contentWidth / 2, cy + contentHeight / 2);
        //绘制文字
        int size = dataList.size();
        int contentPadding = textHeight + textPading;
        int half = maxShowNum / 2 + 1;
        for (int i = -half; i <= half; i++) {
            //当前要绘制的字符对应的下标
            int index = curIndex - offsetIndex + i;
            if (isRecycleMode) {
                if (index < 0)
                    index = (index + 1) % dataList.size() + dataList.size() - 1;
                else if (index > dataList.size() - 1)
                    index = index % dataList.size();
            }
            if (index >= 0 && index < size) {
                //计算每个字的中间坐标
                int tempY = cy + i * contentPadding;
                tempY += offsetY % contentPadding;
                //根据每个字中间y坐标到cy的距离,计算出scale的值
                float scale = 1.0f - (1.0f * Math.abs(tempY - cy) / contentPadding);


                //根据textMaxScale,计算出tempScale,范围为1-textMaxScale
                float tempScale = scale * (textMaxScale - 1.0f) + 1.0f;
                tempScale = tempScale < 1.0f ? 1.0f : tempScale;

                //计算文字的alpha值
                float textAlpha=textMinAlpha;
                if (textMaxScale != 1) {
                    float tempAlpha = (tempScale - 1) / (textMaxScale - 1);
                    textAlpha = (1 - textMinAlpha) * tempAlpha + textMinAlpha;
                }

                textPaint.setTextSize(textSize * tempScale);
                textPaint.setAlpha((int) (255 * textAlpha));

                // 绘制
                Paint.FontMetrics tempFm = textPaint.getFontMetrics();
                String text = dataList.get(index);
                float textWidth = textPaint.measureText(text);
                canvas.drawText(text, cx - textWidth / 2, tempY - (tempFm.ascent + tempFm.descent) / 2, textPaint);

            }
        }
    }

    //标准尺寸转化
    private int getDimension(int unit, float value, Context context) {
        return (int) TypedValue.applyDimension(unit, value, context.getResources().getDisplayMetrics());
    }

    /**
     * 设置要显示的数据
     *
     * @param dataList 要显示的数据
     */
    public void setDataList(ArrayList<String> dataList) {
        this.dataList.clear();
        this.dataList.addAll(dataList);

        // 更新maxTextWidth
        if (null != dataList && dataList.size() > 0) {
            int size = dataList.size();
            for (int i = 0; i < size; i++) {
                float tempWidth = textPaint.measureText(dataList.get(i));
                if (tempWidth > textMaxwidth)
                    textMaxwidth = tempWidth;
            }
            curIndex = 0;
        }
        requestLayout();
        invalidate();
    }

    //添加速率追踪器
    private void  addVelocityTracker(MotionEvent event){
     if (velocityTracker==null)
         velocityTracker=VelocityTracker.obtain();
     velocityTracker.addMovement(event);
 }
 //回收
 private void recycleVelocityTacvker(){
        if (velocityTracker!=null)
            velocityTracker.recycle();
        velocityTracker=null;
    }
    //得到速率值
    private int getScrollYVelocity() {
        velocityTracker.computeCurrentVelocity(1000, maxNumVelocity);
        int velocity = (int) velocityTracker.getYVelocity();
        return velocity;
    }
    //滚动时重新绘制
    private void reDraw() {
        // curIndex需要偏移的量
        int i = (int) (offsetY / (textHeight + textPading));
        if (isRecycleMode || (curIndex - i >= 0 && curIndex - i < dataList.size())) {
            if (offsetIndex != i) {
                offsetIndex = i;

                if (null != onScrollChangedListener)
                    onScrollChangedListener.onScrollChanged(getNowIndex(-offsetIndex),this);
            }
            postInvalidate();
        } else {
            finishScroll();
        }
    }
    //结束滚动
    private void finishScroll() {
        // 判断结束滑动后应该停留在哪个位置
        int centerPadding = textHeight + textPading;
        float v = offsetY % centerPadding;
        if (v > 0.5f * centerPadding)
            ++offsetIndex;
        else if (v < -0.5f * centerPadding)
            --offsetIndex;

        // 重置curIndex
        curIndex = getNowIndex(-offsetIndex);

        // 计算回弹的距离
        bonceDistance = offsetIndex * centerPadding - offsetY;
        offsetY += bonceDistance;

        // 更新
        if (null != onScrollChangedListener)
            onScrollChangedListener.onScrollFinished(curIndex,this);

        // 重绘
        reset();
        postInvalidate();
    }
    //得到当前的选中项
    private int getNowIndex(int offsetIndex) {
        int index = curIndex + offsetIndex;
        if (isRecycleMode) {
            if (index < 0)
                index = (index + 1) % dataList.size() + dataList.size() - 1;
            else if (index > dataList.size() - 1)
                index = index % dataList.size();
        } else {
            if (index < 0)
                index = 0;
            else if (index > dataList.size() - 1)
                index = dataList.size() - 1;
        }
        return index;
    }
    /**
     * 滚动指定的偏移量
     *
     * @param offsetIndex 指定的偏移量
     */
    public void moveBy(int offsetIndex) {
        moveTo(getNowIndex(offsetIndex));
    }
    /**
     * 滚动到指定位置
     *
     * @param index 需要滚动到的指定位置
     */
    public void moveTo(int index) {
        if (index < 0 || index >= dataList.size() || curIndex == index)
            return;

        if (!scroller.isFinished())
            scroller.forceFinished(true);

        finishScroll();

        int dy = 0;
        int centerPadding = textHeight + textPading;
        if (!isRecycleMode) {
            dy = (curIndex - index) * centerPadding;
        } else {
            int offsetIndex = curIndex - index;
            int d1 = Math.abs(offsetIndex) * centerPadding;
            int d2 = (dataList.size() - Math.abs(offsetIndex)) * centerPadding;

            if (offsetIndex > 0) {
                if (d1 < d2)
                    dy = d1; // ascent
                else
                    dy = -d2; // descent
            } else {
                if (d1 < d2)
                    dy = -d1; // descent
                else
                    dy = d2; // ascent
            }
        }
        scroller.startScroll(0, 0, 0, dy, 500);
        invalidate();
    }
//回复数据
    private void reset() {
        offsetY = 0;
        oldOffsetY = 0;
        offsetIndex = 0;
        bonceDistance = 0;
    }

    /**
     * 滚动发生变化时的回调接口
     */
    public interface OnScrollChangedListener {
        public void onScrollChanged(int curIndex,View view);

        public void onScrollFinished(int curIndex,View view);
    }
    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            offsetY = oldOffsetY + scroller.getCurrY();

            if (!scroller.isFinished())
                reDraw();
            else
                finishScroll();
        }
    }
    private OnScrollChangedListener onScrollChangedListener;

    public void setOnScrollChangedListener(OnScrollChangedListener onScrollChangedListener) {
        this.onScrollChangedListener = onScrollChangedListener;
    }
}

Github地址:https://github.com/momoshusheng/GyfPickerView

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容