1. 程式人生 > >Android 自定義SwitchButtonView實踐

Android 自定義SwitchButtonView實踐

開發十年,就只剩下這套架構體系了! >>>   

1、文字繪製基線測量

文字繪製的方法是Canvas類的drawText,對於x點座標其實和正常流程類似,但Y座標的確定需要考慮Baseline問題

@param text The text to be drawn 
@param x X方向的座標,開始繪製的左上角橫軸座標點
@param y Y座標,該座標是Y軸方向上的”基線”座標
@param paint 畫筆工具
*/ 
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)

基線到中線的距離=(Descent+Ascent)/2-Descent ,Android中,實際獲取到的Ascent是負數。

公式推導過程如下: 
中線到BOTTOM的距離是(Descent+Ascent)/2,這個距離又等於Descent+中線到基線的距離,即(Descent+Ascent)/2=基線到中線的距離+Descent。 
有了基線到中線的距離,我們只要知道任何一行文字中線的位置,就可以馬上得到基線的位置,從而得到Canvas的drawText方法中引數y的值。

    /**
     * 計算繪製文字時的基線到中軸線的距離,Android獲取中線到基線距離的程式碼,Paint需要設定文字大小textsize。
     * 
     * @param p
     * @param centerY
     * @return 基線和centerY的距離
     */
    public static float getBaseline(Paint p) {
        FontMetrics fontMetrics = p.getFontMetrics();
        return (fontMetrics.descent - fontMetrics.ascent) / 2 -fontMetrics.descent;
    }

說道這裡我們只是計算出了基線高度,Y座標一般區文字高度的中點位置。比如豎直方向,公式為。

Y = centerY + getBaseline(paint); 

此外,對於寬度的測量,一般使用如下方法

 mPaint.getTextBounds(text, 0, text.length(), mBounds);
 float textwidth = mBounds.width();

 

2、Path閉合區域填充問題

在常見的繪製View的過程中,我們通過Path物件構建複雜的閉合影象,最後一般來通過Paint設定Style.FILL填充區域,但是對於閉合的Path填充,在Android某些版本中不支援填充Path的區域。實際上Path同樣提供了填充方法,可以做到很好的相容。

Android的Path.FillType除了支援上面兩種模式外,還支援了上面兩種模式的反模式,一共定義了EVEN_ODD, INVERSE_EVEN_ODD, WINDING, INVERSE_WINDING 四種模式。

實際上,WINDING類似Paint中的Style.FILL

 

3、Path 影象合成

一般情況下我們影象是將Bitmap合成,合成時使用Xfermodes,當然Path也可以轉為Bitmap影象資料。

但是Path同樣提供了一系列合成方法

DIFFERENCE:從path1中減去path2

INTERSECT:取path1和path2重合的部分

REVERCE_DIFFERENCE:從path2中減去path1

UNION:聯合path1和path2

XOR:取path1和path2不重合的部分


 

4、StrokeWidth與區域大小問題

對於帶邊框的View,StrokeWidth在很多情況下被認為不擠佔區域大小,實際上,與此相反,我們計算座標時一定要計算線寬問題。比如繪製線寬StrokeWidth的起點矩形,如果不這樣計算,繪製將會出現邊框寬度不一致的情況。

startX = StrokeWidth;

startY = StrokeWidth;

endX = getWidth() - StrokeWidth;

endY = getHeight- StrokeWidth;

 

5、觸控MOVE事件問題

很多時候繪製View我們需要處理TouchEvent事件,然而,Android中View預設無法監聽,需要設定一個莫名其妙的引數。

setClickable(true);

 

5、事件狀態轉移問題

很多時候,我們判斷到某一區域時達到某種條件需要主動結束事件事務,或者改變事件狀態如下然後在傳遞出去,方法如下

  MotionEvent actionUP = MotionEvent.obtain(event); //增量式拷貝,比如修修改開始時間、修改修改時間序列
  actionUP.setAction(MotionEvent.ACTION_UP);

  dispatchTouchEvent(actionUP); //傳遞事件,注意不要造成死迴圈問題

 

基於以上問題的解決,實現了一個SwitchButton,雖然沒用到Path,但還是考慮了很多問題。

public class SwitchButtonView extends View  {

    // 例項化畫筆
    private TextPaint mPaint = null;
    private Path mPath;// 路徑物件
    private int lineWidth  = 1;

    private final  int STATUS_LEFT = 0x00;
    private final  int STATUS_RIGHT = 0x01;
    private volatile  int mStatus = STATUS_LEFT;

    private int textSize = 18;

    private volatile float startX = 0; //觸控開始位置
    private volatile boolean isTouchState = false;
    private volatile float currentX = 0;
    private final  String[]  STATUS = {"開","關"};
    private OnStatusChangedListener mOnStatusChangedListener;

    public void setLeftText(String text){
        STATUS[0] = text;
    }
    public void setRightText(String text){
        STATUS[1] = text;
    }

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

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

    public SwitchButtonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
        setClickable(true); //設定此項true,否則無法滑動
    }
    private void initPaint() {
        // 例項化畫筆並開啟抗鋸齒
        mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG );
        mPaint.setAntiAlias(true);
        mPaint.setPathEffect(new CornerPathEffect(10)); //設定線條型別
        mPaint.setStrokeWidth(dpTopx(lineWidth));
        mPaint.setTextSize(dpTopx(textSize));
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        if(widthMode!=MeasureSpec.EXACTLY){
            width = (int) dpTopx(105*2);
        }
        if(heightMode!=MeasureSpec.EXACTLY){
            height = (int) dpTopx(35*2);
        }
        setMeasuredDimension(width,height);

    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        if(width<=0 || height<=0) return;

        int centerX = width/2;
        int centerY = height/2;

        int lineWidthPixies = (int) dpTopx(lineWidth);
        int R = getHeight()/2;
        mPaint.setStyle(Paint.Style.STROKE);
        int startX = lineWidthPixies;
        int startY = lineWidthPixies;
        int endX = width - 2*lineWidthPixies; //寬度應該減去左右兩邊的線寬
        int endY = height - 2*lineWidthPixies; //寬度應該減去上下兩邊的線寬

        canvas.drawRoundRect(new RectF(startX,startY,endX,endY),R,R,mPaint);

        //中間分割線
        canvas.drawLine(centerX,height*2/5,centerX,height*3/5,mPaint);

        drawText(canvas, width, centerY);

        drawSlider(canvas,width,height,lineWidthPixies);

    }

    private void drawText(Canvas canvas, int width, int centerY) {
        Rect mBounds = new Rect();
        mPaint.getTextBounds(STATUS[0], 0, STATUS[0].length(), mBounds);
        float textwidth = mBounds.width();
        float textBaseline = centerY + getTextPaintBaseline(mPaint);

        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawText(STATUS[0],width/4-textwidth/2,textBaseline,mPaint);
        canvas.drawText(STATUS[1],width*3/4-textwidth/2, textBaseline,mPaint);//文字位置以基線為準
        mPaint.setStyle(Paint.Style.STROKE);
    }
    /**
     * 基線到中線的距離=(Descent+Ascent)/2-Descent
     注意,實際獲取到的Ascent是負數。公式推導過程如下:
     中線到BOTTOM的距離是(Descent+Ascent)/2,這個距離又等於Descent+中線到基線的距離,即(Descent+Ascent)/2=基線到中線的距離+Descent。
     */
    public static float getTextPaintBaseline(Paint p) {
        Paint.FontMetrics fontMetrics = p.getFontMetrics();
        return (fontMetrics.descent - fontMetrics.ascent) / 2 -fontMetrics.descent;
    }


    private void drawSlider(Canvas canvas, int outwidth, int outheight, int lineWidthPixies) {
        int color = mPaint.getColor();
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.FILL);
        float width = outwidth - 2*lineWidthPixies;
        float height = outheight - 2 * lineWidthPixies;

        int slideBarX =  2* lineWidthPixies;
        int slideBarY =  2*lineWidthPixies;
        int R = (int) (height/2);

        if(isTouchState){

            canvas.drawRoundRect(new RectF(currentX, slideBarY, currentX+width/2-3*lineWidthPixies, height - lineWidthPixies), R, R, mPaint);

        }else {
            if (mStatus == STATUS_RIGHT) {
                slideBarX = (int) (slideBarY+width/2+lineWidthPixies);
                canvas.drawRoundRect(new RectF(slideBarX, slideBarY, width - lineWidthPixies, height - lineWidthPixies), R, R, mPaint);
            } else {
                canvas.drawRoundRect(new RectF(slideBarX, slideBarY, width / 2 - lineWidthPixies, height - lineWidthPixies), R, R, mPaint);
            }
        }
        mPaint.setColor(color);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float lineWidthPixies = dpTopx(lineWidth);

        float width = (getWidth()- 2*lineWidthPixies);
        float sliderWidth = width/2;
        int actionMasked = event.getActionMasked();
        switch (actionMasked){
            case MotionEvent.ACTION_DOWN: {
                isTouchState = true;
                startX = event.getX();
                if (startX > (width / 2) && startX<(width-lineWidthPixies) && mStatus == STATUS_LEFT) {
                    MotionEvent actionUP = MotionEvent.obtain(event);
                    actionUP.setAction(MotionEvent.ACTION_UP);
                    dispatchTouchEvent(actionUP);
                } else if (startX > lineWidthPixies && (startX < width / 2 && mStatus == STATUS_RIGHT)) {
                    MotionEvent actionUP = MotionEvent.obtain(event);
                    actionUP.setAction(MotionEvent.ACTION_UP);
                    dispatchTouchEvent(actionUP);
                }else if(startX<lineWidthPixies || startX>(width-lineWidthPixies)){
                    MotionEvent actionOUTSIDE = MotionEvent.obtain(event);
                    actionOUTSIDE.setAction(MotionEvent.ACTION_OUTSIDE);
                    dispatchTouchEvent(actionOUTSIDE);
                }
            }
                break;
            case MotionEvent.ACTION_MOVE:
                currentX = event.getX()- sliderWidth/2;
                //滑塊移動位置應該相對於中心位置為基準
                if(currentX<(2* lineWidthPixies)){
                    currentX = 2* lineWidthPixies; //最左邊
                }else if(currentX>((lineWidthPixies+sliderWidth)+2*lineWidthPixies)){ //最右邊
                    currentX = (sliderWidth)+2*lineWidthPixies;
                }
                postInvalidate();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_OUTSIDE:
                isTouchState = false;
                float xPos = event.getX();
                if((xPos>width/2&& mStatus==STATUS_LEFT)){
                    mStatus = STATUS_RIGHT;
                    onStatusChanged(mStatus);
                }else if(xPos>lineWidthPixies && (xPos<width/2&& mStatus==STATUS_RIGHT)){
                    mStatus = STATUS_LEFT;
                    onStatusChanged(mStatus);
                }
                invalidate();

                break;
        }

        return super.onTouchEvent(event);
    }

    private void onStatusChanged(int status) {

        if(this.mOnStatusChangedListener!=null){
            this.mOnStatusChangedListener.onStatusChanged(status);
        }
    }

    private float dpTopx(int dp){
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp,getResources().getDisplayMetrics());
    }


    public void setOnStatusChangedListener(OnStatusChangedListener l){
        this.mOnStatusChangedListener = l;
    }


    interface OnStatusChangedListener{

        void onStatusChanged(int status);
    }
}