Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義標尺控件RulerView

Android自定義標尺控件RulerView

編輯:關於Android編程

讓用戶直接輸入身高體重,這種體驗真是太糟糕啦。我們不妨讓用戶啟動手指滑動標尺來確定他的身高體重,這樣不是更有趣麼?

 

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;
    }
}

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved