Android自定义标尺控件RulerView

让用户直接输入身高体重,这种体验真是太糟糕啦。我们不妨让用户启动手指滑动标尺来确定他的身高体重,这样不是更有趣么?

package com.lw.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.IntegerRes;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.OverScroller;

import com.lw.R;

/**
 * 标尺类
 */
public class RulerView extends View {
    //getSimpleName()返回源代码中给出的底层类的简称。
    final String TAG = RulerView.class.getSimpleName();
    //开始范围
    private int mBeginRange;
    //结束范围
    private int mEndRange;
    /**内部宽度,也就是标尺每条的宽度*/
    private int mInnerWidth;
    //标尺条目间的间隔
    private int mIndicatePadding;
    //显示的画笔
    private Paint mIndicatePaint;
    //文字画笔
    private Paint mTextPaint;
    //显示的宽度
    private int mIndicateWidth;
    //显示的大小
    private float mIndicateScale;
    //最后的手势的X坐标
    private int mLastMotionX;
    /**是否可以滑动*/
    private boolean mIsDragged;
    //是否自动匹配
    private boolean mIsAutoAlign = true;
    //是否需要显示文字
    private boolean mIsWithText = true;
    //文字颜色
    private int mTextColor;
    //文字大小
    private float mTextSize;
    //标尺的颜色
    private int mIndicateColor;
    //大小比例监听器
    private OnScaleListener mListener;
    //标尺条显示的位置:top,bottom
    private int mGravity;
    /**标尺矩形(刻度条)*/
    private Rect mIndicateLoc;

    /**滚动相关参数,这个类封装了滚动与超能力的界限*/
    private OverScroller mOverScroller;
    /**帮助跟踪触摸事件的速度,用于执行投掷等动作。*/
    private VelocityTracker mVelocityTracker;
    /**触摸溢出*/
    private int mTouchSlop;
    //最小滑动速率
    private int mMinimumVelocity;
    //最大速率
    private int mMaximumVelocity;




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

    public RulerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * 最终都是调用此构造方法
     *
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public RulerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //获取自定义属性数据集,并写入缺省值,自定义了8个属性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RulerView);
        mIndicateColor = ta.getColor(R.styleable.RulerView_indicateColor, Color.BLACK);
        mTextColor = ta.getColor(R.styleable.RulerView_textColor, Color.GRAY);
        mTextSize = ta.getDimension(R.styleable.RulerView_textSize, 18);
        mBeginRange = ta.getInt(R.styleable.RulerView_begin, 0);
        mEndRange = ta.getInt(R.styleable.RulerView_end, 100);
        //标尺宽度
        mIndicateWidth = (int) ta.getDimension(R.styleable.RulerView_indicateWidth, 5);
        //标尺的间隙
        mIndicatePadding = (int) ta.getDimension(R.styleable.RulerView_indicatePadding, 15);
        ta.recycle();
        //标尺条显示的位置,缺省值为显示在底部
        int[] indices = new int[]{android.R.attr.gravity};
        ta = context.obtainStyledAttributes(attrs, indices);
        mGravity = ta.getInt(ta.getIndex(0), Gravity.BOTTOM);
        ta.recycle();
        //默认显示比例为0.7倍
        mIndicateScale = 0.7f;

        initValue();
    }

    /**
     * 初始化数值
     */
    private void initValue() {
        /**  创建这个滚动类,并设置滚动模式为:1.OVER_SCROLL_ALWAYS 标准模式
         * 还有两种滚动模式为:2.OVER_SCROLL_IF_CONTENT_SCROLLS 内容滚动
         * 3.OVER_SCROLL_NEVER 不滚动
         */
        mOverScroller = new OverScroller(getContext());
        setOverScrollMode(OVER_SCROLL_ALWAYS);
        //获取视图配置,设置触摸溢出,和最小和最大的触摸速率
        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mTouchSlop = configuration.getScaledTouchSlop();
        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();

        //设置标尺的画笔,实心画
        mIndicatePaint = new Paint();
        mIndicatePaint.setStyle(Paint.Style.FILL);
        //设置文字画笔,实心画,并消除锯齿
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        mTextPaint.setTextSize(mTextSize);
        //内部宽度(标尺结束范围-标尺开始范围)*指示宽度
        mInnerWidth = (mEndRange - mBeginRange) * getIndicateWidth();
        //标尺定位为一个矩形
        mIndicateLoc = new Rect();


    }



    /**
     * 重写绘制方法
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {

        /**
         * 当我们对画布进行旋转,缩放,平移等操作的时候其实我们是想对特定的元素进行操作,
         * 比如图片,一个矩形等,但是当你用canvas的方法来进行这些操作的时候,
         * 其实是对整个画布进行了操作,那么之后在画布上的元素都会受到影响,
         * 所以我们在操作之前调用canvas.save()来保存画布当前的状态,
         * 当操作之后取出之前保存过的状态,这样就不会对其他的元素进行影响
         */
        int count = canvas.save();
        //循环绘制标尺条(刻度),根据最大值和最小值来绘制
        for (int value = mBeginRange, position = 0; value <= mEndRange; value++, position++) {
            drawIndicate(canvas, position);
            //如果需要数字,还需要在刻度下绘制数字
            if (mIsWithText)
                drawText(canvas, position, String.valueOf(value));

        }

        //恢复Canvas的状态
        canvas.restoreToCount(count);

    }

    /**
     * 绘制标尺条(刻度),0到100就会显示100个刻度
     * @param canvas 画布
     * @param position
     */
    private void drawIndicate(Canvas canvas, int position) {
        computeIndicateLoc(mIndicateLoc, position);
        int left = mIndicateLoc.left + mIndicatePadding;
        int right = mIndicateLoc.right - mIndicatePadding;
        int top = mIndicateLoc.top;
        int bottom = mIndicateLoc.bottom;

        if (position % 5 != 0) {
            int indicateHeight = bottom - top;
            if (isAlignTop()) {
                bottom = (int) (top + indicateHeight * mIndicateScale);
            } else {
                top = (int) (bottom - indicateHeight * mIndicateScale);
            }
        }

        mIndicatePaint.setColor(mIndicateColor);
        canvas.drawRect(left, top, right, bottom, mIndicatePaint);
    }

    /**
     * 绘制文字,每5个刻度绘制一个文字用于提示
     * @param canvas
     * @param position
     * @param text
     */
    private void drawText(Canvas canvas, int position, String text) {
        if (position % 5 != 0)
            return;

        computeIndicateLoc(mIndicateLoc, position);
        int textHeight = computeTextHeight();

        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);
        mTextPaint.setTextAlign(Paint.Align.CENTER);

        int x = (mIndicateLoc.left + mIndicateLoc.right) / 2;
        int y = mIndicateLoc.bottom + textHeight;

        if (!isAlignTop()) {
            y = mIndicateLoc.top;
            mTextPaint.getTextBounds(text, 0, text.length(), mIndicateLoc);
            y += mIndicateLoc.top / 2;  //增加一些偏移
        }

        canvas.drawText(text, x, y, mTextPaint);
    }

    /**
     * 计算指示器的位置:设置左上右下
     * 最终是设置了此矩形(刻度的左上右下)
     * @param outRect 矩形
     * @param position 位置数值(代表第几个刻度)
     */
    private void computeIndicateLoc(Rect outRect, int position) {
        if (outRect == null)
            return;

        int height = getHeight();
        int indicate = getIndicateWidth();

        int left = (indicate * position);
        int right = left + indicate;
        int top = getPaddingTop();//获得当前View的顶内距
        int bottom = height - getPaddingBottom();//视图高度-视图低内距

        if (mIsWithText) {
            int textHeight = computeTextHeight();
            if (isAlignTop())
                bottom -= textHeight;//如果是刻度显示在顶部,底部要减去文字的高度
            else
                top += textHeight;//如果是刻度显示在底部,顶部要加上文字的高度
        }
        //文字偏移量,左边和右边都加上一个偏移量
        int offsets = getStartOffsets();
        left += offsets;
        right += offsets;
        outRect.set(left, top, right, bottom);
    }

    /**
     * 开始偏移,如果要包含文字的话才需要偏移。
     *
     * @return
     */
    private int getStartOffsets() {
        if (mIsWithText) {
            String text = String.valueOf(mBeginRange);
            //返回文字的宽度
            int textWidth = (int) mTextPaint.measureText(text, 0, text.length());
            return textWidth / 2;//实际偏移文字宽度的一半,使其居中显示
        }
        return 0;
    }

    /**
     * 触摸相关事件
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        //如果不存在初始速度跟踪
        initVelocityTrackerIfNotExists();
        //速度追踪者 添加移动事件
        mVelocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //按下时如果滑动还没结束
                if (mIsDragged = !mOverScroller.isFinished()) {
                    if (getParent() != null)
                        //要求禁止拦截触摸事件
                        getParent().requestDisallowInterceptTouchEvent(true);
                }
                //如果动画没结束,就结束动画
                if (!mOverScroller.isFinished())
                    mOverScroller.abortAnimation();
                //记录按下的x的坐标
                mLastMotionX = (int) event.getX();

                return true;

            case MotionEvent.ACTION_MOVE:
                //移动时x的值,并得到(按下x值-移动x)值的差值
                int curX = (int) event.getX();
                int deltaX = mLastMotionX - curX;
                //如果滑动未结束,且移动距离的绝对值大于触摸溢出量
                if (!mIsDragged && Math.abs(deltaX) > mTouchSlop) {
                    if (getParent() != null)
                        //如果有父级控件,就告诉父级控件不要拦截我的触摸事件
                        getParent().requestDisallowInterceptTouchEvent(true);
                    //并设置滑动结束
                    mIsDragged = true;
                    //如果触摸差值》0,触摸差值需要-触摸溢出量,否则加上
                    if (deltaX > 0) {
                        deltaX -= mTouchSlop;
                    } else {
                        deltaX += mTouchSlop;
                    }
                }
                //如果滑动结束,最后的x坐标就是当前触摸的的点
                if (mIsDragged) {
                    mLastMotionX = curX;
                    //如果滚动的X值《0或者大于最大的滚动值了,让触摸差值*0.7
                    if (getScrollX() <= 0 || getScrollX() >= getMaximumScroll())
                        deltaX *= 0.7;
                    //滚动超出正常的标准行为的视图,速率监听清除?????????????
                    if (overScrollBy(deltaX, 0, getScrollX(), getScrollY(), getMaximumScroll(), 0, getWidth(), 0, true)) {
                        mVelocityTracker.clear();
                    }

                }

                break;
            case MotionEvent.ACTION_UP: {
                if (mIsDragged) {
                    //检查滑动的速度,1000单位
                    mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    //获得X轴上的流速
                    int initialVelocity = (int) mVelocityTracker.getXVelocity();
                    //如果x轴流速》最小流速
                    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                        fling(-initialVelocity);
                    } else {
                        //alignCenter();
                        //回弹到末尾
                        sprintBack();
                    }
                }
                //滑动结束
                mIsDragged = false;
                //释放追踪器资源
                recycleVelocityTracker();
                break;
            }
            case MotionEvent.ACTION_CANCEL: {
                //如果滑动结束,且滚动结束,就回滚
                if (mIsDragged && mOverScroller.isFinished()) {
                    sprintBack();
                }

                mIsDragged = false;

                recycleVelocityTracker();
                break;
            }
        }

        return true;
    }

    /**
     * 刷新参数值
     */
    private void refreshValues() {
        //内部宽度 = (最大值-开始值)*刻度宽度
        mInnerWidth = (mEndRange - mBeginRange) * getIndicateWidth();
        invalidateView();

    }

    /**
     * 最终指示宽度 :刻度宽度+刻度内边距+刻度内边距
     *
     * @return
     */
    private int getIndicateWidth() {
        return mIndicateWidth + mIndicatePadding + mIndicatePadding;
    }

    /**
     * 获取最小滚动值。
     *
     * @return
     */
    private int getMinimumScroll() {
        return -(getWidth() - getIndicateWidth()) / 2 + getStartOffsets();
    }

    /**
     * 获取最大滚动值。
     *
     * @return
     */
    private int getMaximumScroll() {
        return mInnerWidth + getMinimumScroll();
    }

    /**
     * 调整刻度,使其居中。
     */
    private void adjustIndicate() {
        if (!mOverScroller.isFinished())
            mOverScroller.abortAnimation();

        int position = computeSelectedPosition();
        int scrollX = getScrollByPosition(position);
        scrollX -= getScrollX();

        if (scrollX != 0) {
            //滚动边界开始滚动
            mOverScroller.startScroll(getScrollX(), getScrollY(), scrollX, 0);
            invalidateView();
        }
    }

    /**
     * 投掷
     * @param velocityX
     * 根据x轴滑动速率,来回退刷新界面
     */
    public void fling(int velocityX) {
        mOverScroller.fling(getScrollX(), getScrollY(), velocityX, 0, getMinimumScroll(), getMaximumScroll(), 0, 0, getWidth() / 2, 0);
        invalidateView();
    }

    /**
     * 回弹
     */
    public void sprintBack() {
        mOverScroller.springBack(getScrollX(), getScrollY(), getMinimumScroll(), getMaximumScroll(), 0, 0);
        invalidateView();
    }


    public void setOnScaleListener(OnScaleListener listener) {
        if (listener != null) {
            mListener = listener;
        }
    }

    /**
     * 获取position的绝对滚动位置。
     *
     * @param position
     * @return
     */
    private int getScrollByPosition(int position) {
        computeIndicateLoc(mIndicateLoc, position);
        int scrollX = mIndicateLoc.left - getStartOffsets() + getMinimumScroll();
        return scrollX;
    }

    /**
     * 计算当前已选择的位置
     *
     * @return
     */
    public int computeSelectedPosition() {
        //计算出两个刻度的中间位置
        int centerX = getScrollX() - getMinimumScroll() + getIndicateWidth() / 2;
        //通过中间位置来判断选择的刻度值位置
        centerX = Math.max(0, Math.min(mInnerWidth, centerX));
        int position = centerX / getIndicateWidth();
        return position;
    }

    /**
     * 平滑滚动
     * @param position
     */
    public void smoothScrollTo(int position) {
        //如果选择的位置<0或者开始值+选择位置大于最终值,就直接返回吧
        if (position < 0 || mBeginRange + position > mEndRange)
            return;
        //如果滚动没有完成,中断它的动画吧
        if (!mOverScroller.isFinished())
            mOverScroller.abortAnimation();

        int scrollX = getScrollByPosition(position);
        mOverScroller.startScroll(getScrollX(), getScrollY(), scrollX - getScrollX(), 0);
        invalidateView();
    }

    /**
     * 平滑滚动到的值
     * @param value
     */
    public void smoothScrollToValue(int value) {
        int position = value - mBeginRange;
        smoothScrollTo(position);
    }

    /**
     * 触发放大缩小事件
     * @param scale
     */
    private void onScaleChanged(int scale) {
        if (mListener != null)
            mListener.onScaleChanged(scale);
    }

    /**
     * 重新在滚动时的事件
     * @param scrollX
     * @param scrollY
     * @param clampedX 固定的x
     * @param clampedY 固定的Y
     */
    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        //如果滚动没有完成,设置滚动x参数,并监听滚动
        if (!mOverScroller.isFinished()) {
            final int oldX = getScrollX();
            final int oldY = getScrollY();
            setScrollX(scrollX);
            onScrollChanged(scrollX, scrollY, oldX, oldY);
            if (clampedX) {
                //sprintBack();
            }
        } else {
            super.scrollTo(scrollX, scrollY);
        }
        //如果监听器不为null,赋值当前选择的位置,并触发缩放改变事件
        if (mListener != null) {
            int position = computeSelectedPosition();
            onScaleChanged(position + mBeginRange);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);


    }

    /**
     * 计算文字高度
     * @return
     */
    private int computeTextHeight() {
        //使用FontMetrics对象,计算文字的坐标。
        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
        float textHeight = fontMetrics.descent - fontMetrics.ascent;
        return (int) textHeight;
    }

    private boolean isAlignTop() {
        //&为位运算符,就是32位二进制值得比较
        return (mGravity & Gravity.TOP) == Gravity.TOP;
    }

    public void setGravity(int gravity) {
        this.mGravity = gravity;
        invalidateView();
    }

    /**
     * 计算滚动
     */
    @Override
    public void computeScroll() {
        if (mOverScroller.computeScrollOffset()) {
            int oldX = getScrollX();
            int oldY = getScrollY();
           // 返回滚动中的电流偏移量,百度居然这么翻译
            int x = mOverScroller.getCurrX();
            int y = mOverScroller.getCurrY();
            //滚动过多得操作
            overScrollBy(x - oldX, y - oldY, oldX, oldY, getMaximumScroll(), 0, getWidth(), 0, false);
            invalidateView();
        } else if (!mIsDragged && mIsAutoAlign) {//如果不再滚动且开启了自动对齐
            adjustIndicate();
        }
    }

    @Override
    protected int computeHorizontalScrollRange() {
        return getMaximumScroll();
    }

    /**
     * 刷新界面
     * 如果版本大于16(4.1)
     * 使用postInvalidate可以直接在线程中更新界面
     * invalidate()必须在UI线程中使用
     */
    public void invalidateView() {
        if (Build.VERSION.SDK_INT >= 16) {
            postInvalidateOnAnimation();
        } else
            invalidate();
    }

    /**
     * 获得周转率追踪器
     */
    private void initVelocityTrackerIfNotExists() {
        if (mVelocityTracker == null) {
            //获得当前周转率追踪
            mVelocityTracker = VelocityTracker.obtain();
        }
    }
    /**
     * 释放 周转率追踪器资源
     */
    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    /**
     * 放大缩小监听接口
     */
    public interface OnScaleListener {
        void onScaleChanged(int scale);

    }

    /**
     * 设置刻度的宽度
     * @param indicateWidth
     */
    public void setIndicateWidth(@IntegerRes int indicateWidth) {
        this.mIndicateWidth = indicateWidth;
        refreshValues();
    }

    /**
     * 设置刻度内间距
     * @param indicatePadding
     */
    public void setIndicatePadding(@IntegerRes int indicatePadding) {
        this.mIndicatePadding = indicatePadding;
        refreshValues();
    }

    public void setWithText(boolean withText) {
        this.mIsWithText = withText;
        refreshValues();
    }

    public void setAutoAlign(boolean autoAlign) {
        this.mIsAutoAlign = autoAlign;
        refreshValues();
    }

    /**
     * 是否显示文字
     * @return
     */
    public boolean isWithText() {
        return mIsWithText;
    }

    /**
     * 自动对齐刻度
     * @return
     */
    public boolean isAutoAlign() {
        return mIsAutoAlign;
    }
}

源码地址: RulerView

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值