自定義View基礎與原理

這裏寫鏈接內容# 自定義View基礎與原理


什麼是自定義View

其實就是繼承系統的View,然後加入繪製元素(文字/圖形)和邏輯,最終達到自己想要想過的控件。


爲什麼使用自定義View

  • 特定的顯示風格
  • 處理特有的用戶交互
  • 優化佈局
  • 封裝等

如何自定義控件

編寫自己的自定義View

- 編寫最簡單的自定義View,什麼都不顯示,但是有View的特性
/**
     * java代碼創建視圖的時候被調用,如果是從xml填充的視圖,就不會調用這個
     * 
     * @param context
     */
    public MyView(Context context) {
        super(context);
    }

    /**
     * 這個是在xml創建但是沒有指定style的時候被調用
     * 
     * @param context
     * @param attrs
     */
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 這個是在xml創建且指定style的時候被調用
     * 
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

那種情況下調用那個構造方法,上面註釋已經寫的很清楚了。不妨我們想一下,讓第一個構造方法去調用第二個構造方法,第二個去調用第三個,這樣會不會省去一些事呢。如下面例子的代碼所示。

- 可以顯示自己的元素(文字/幾何圖形/圖片)
public class MyView extends View {

    private Paint paint;
    private Bitmap bitmap;

    /**
     * java代碼創建視圖的時候被調用,如果是從xml填充的視圖,就不會調用這個
     * 
     * @param context
     */
    public MyView(Context context) {
        this(context, null);
    }

    /**
     * 這個是在xml創建但是沒有指定style的時候被調用
     * 
     * @param context
     * @param attrs
     */
    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * 這個是在xml創建且指定style的時候被調用
     * 
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init();
    }

    private void init() {
        // Paint樣式和顏色信息,關於文字/幾何圖形和bitmap的
        paint = new Paint();

        bitmap = BitmapFactory.decodeResource(getResources(),
                R.drawable.ic_launcher);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setTextSize(30);
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.STROKE);
        // 繪製文字
        canvas.drawText("this is onDraw", 0, 30, paint);

        // 繪製直線
        canvas.drawLine(0, 60, 100, 60, paint);

        // 繪製矩形
        canvas.drawRect(0, 90, 100, 190, paint);
        // Rect中參數int類型
        Rect r = new Rect(100, 90, 200, 190);
        canvas.drawRect(r, paint);
        // Rectf中參數float類型
        RectF rect = new RectF(200, 90, 300, 190);
        canvas.drawRect(rect, paint);
        // 圓角矩形rx, ry x和y方向上的弧度
        RectF rect2 = new RectF(300, 90, 400, 190);
        canvas.drawRoundRect(rect2, 30, 30, paint);

        // 繪製圓形
        canvas.drawCircle(50, 270, 50, paint);

        // 繪製圖片
        canvas.drawBitmap(bitmap, 0, 350, paint);
    }

}

佈局中使用

 <!-- 完整的包名和類名 -->
    <com.example.myview.MyView 
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

運行結果如圖:
運行結果

加入邏輯線程

  • 怎麼讓元素動起來呢,需要什麼
    不斷的改變元素繪製的座標位置,我們在視覺上就會感覺到元素在運動
  • 元素動起來的邏輯放在哪裏
    創建一個線程,執行run方法裏面改變其座標,在進行重新繪製,android提供了在線程中重新繪製的方法postInvalidate()
  • 怎樣看起來動起來的元素流暢
    一秒內繪製20次即可
public class LogicView extends View {
    private Paint paint;
    private float rx;
    /**
     * 邏輯線程
     */
    private MyThread mThread;
    private RectF rectF;
    /**
     * 區間角度
     */
    private float sweepAngle;

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

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

    public LogicView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init();
    }

    /**
     * 對類成員進行初始化
     */
    private void init() {
        paint = new Paint();
        rectF = new RectF(0, 60, 100, 160);
        mThread = new MyThread();
        mThread.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setTextSize(30);
        canvas.drawText("LogicView", rx, 30, paint);

        // 起始角度/區間角度
        canvas.drawArc(rectF, 0, sweepAngle, true, paint);
    }

    class MyThread extends Thread {
        Random random = new Random();

        @Override
        public void run() {
            super.run();
            // 不斷的進行繪製
            while (true) {
                long start = System.currentTimeMillis();
                // 超出屏幕長度
                if ((rx += 3) > getWidth()) {
                    rx = 0 - paint.measureText("LogicView");
                }
                // 超出360度,清零
                if (sweepAngle++ > 360) {
                    sweepAngle = 0;
                }

                int r = random.nextInt(256);
                int g = random.nextInt(256);
                int b = random.nextInt(256);
                paint.setARGB(255, r, g, b);

                // 線程中更新繪製的方法
                // postInvalidate();
                invalidateView();

                long end = System.currentTimeMillis();
                // 一秒繪製20次即可
                if (end - start < 50) {
                    try {
                        Thread.sleep(50 - (end - start));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    /**
     * 重繪
     */
    private void invalidateView() {
        if (Looper.getMainLooper() == Looper.myLooper()) {
            invalidate();
        } else {
            postInvalidate();
        }
    }
}

提取和封裝自定義View

  • 封裝後的基類
public abstract class BaseView extends View {
    /**
     * 邏輯線程
     */
    private MyThread mThread;

    /**
     * 線程的控制開關
     */
    private boolean isRunning = true;

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

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

    public BaseView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    // 這裏使用final關鍵字,不讓子類重寫
    @Override
    protected final void onDraw(Canvas canvas) {
        if (mThread == null) {
            mThread = new MyThread();
            mThread.start();
        } else {
            drawSub(canvas);
        }
    }

    /**
     * 繪製元素
     * 
     * @param canvas
     */
    protected abstract void drawSub(Canvas canvas);

    /**
     * 邏輯處理
     */
    protected abstract void logic();

    //離開屏幕的時候結束掉線程
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        isRunning = false;
    }

    class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            // 不斷的進行繪製
            while (isRunning) {
                long start = System.currentTimeMillis();
                logic();

                invalidateView();

                long end = System.currentTimeMillis();
                // 一秒繪製20次即可
                if (end - start < 50) {
                    try {
                        Thread.sleep(50 - (end - start));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    /**
     * 重繪
     */
    public void invalidateView() {
        if (Looper.getMainLooper() == Looper.myLooper()) {
            invalidate();
        } else {
            postInvalidate();
        }
    }
}

該過程中我們提出的繪製元素drawSub(Canvas canvas)方法和邏輯處理logic()方法。並將onDraw使用final關鍵字,不讓子類去重寫。當繪製離開屏幕的時候,結束掉邏輯處理線程。

使用基類

public class LogicView extends BaseView {

    private Paint paint;
    private float rx;
    private RectF rectF;
    /**
     * 區間角度
     */
    private float sweepAngle;

    private Random random;

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

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

    public LogicView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init();
    }

    /**
     * 對類成員進行初始化
     */
    protected void init() {
        paint = new Paint();
        rectF = new RectF(0, 60, 100, 160);

        random = new Random();
    }

    @Override
    protected void drawSub(Canvas canvas) {
        paint.setTextSize(30);
        canvas.drawText("LogicView", rx, 30, paint);

        // 起始角度/區間角度
        canvas.drawArc(rectF, 0, sweepAngle, true, paint);

    }

    @Override
    protected void logic() {
        // 超出屏幕長度
        if ((rx += 3) > getWidth()) {
            rx = 0 - paint.measureText("LogicView");
        }
        // 超出360度,清零
        if (sweepAngle++ > 360) {
            sweepAngle = 0;
        }

        int r = random.nextInt(256);
        int g = random.nextInt(256);
        int b = random.nextInt(256);
        paint.setARGB(255, r, g, b);
    }
}

運行結果如圖:
這裏寫圖片描述

定義XML中定義的樣式來影響顯示效果

  • 自定義樣式attr.xml
    新建自定義樣式attr.xml在res/values目錄下
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="NumText">

        <!-- 繪製的行數 -->
        <attr name="lineNum" format="integer"></attr>
        <!-- 文字是否滾動顯示 -->

        <attr name="isScrool" format="boolean"></attr>
    </declare-styleable>

</resources>
  • 在佈局中使用
    在使用自定義屬性的時候先聲明xml的命名空間
    • eclipse:
      xmlns:myview=”http://schemas.android.com/apk/res/完整的包名”
    • android studio:
      xmlns:myview=”http://schemas.android.com/apk/res-auto”
      佈局中的使用:
    <com.example.myview.v4.NumText
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        myview:isScrool="true"
        myview:lineNum="3" >
    </com.example.myview.v4.NumText>
  • 例子代碼
public class NumText extends BaseView {
    /**
     * 畫筆
     */
    private Paint paint;
    /**
     * 繪製文字x座標
     */
    private float rx;
    /**
     * 繪製的行數
     */
    private int lintNum;
    private Random random;
    /**
     * 是否滾動
     */
    private boolean isScrool;

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

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

    public NumText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        paint = new Paint();
        random = new Random();
        //默認不滾動
        isScrool = false;
        //在view構造方法中獲取自定義屬性
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.NumText);

        int n = a.getIndexCount();

        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
            case R.styleable.NumText_lineNum:
                lintNum = (int) a.getInt(attr, 1);
                break;

            case R.styleable.NumText_isScrool:
                isScrool = (boolean) a.getBoolean(attr, false);
                break;
            }

        }

        a.recycle();
    }

    @Override
    protected void drawSub(Canvas canvas) {
        // 繪製lineNum行文字
        for (int i = 0; i < lintNum; i++) {
            int textSize = 30 + i;
            paint.setTextSize(textSize);
            canvas.drawText("自定義View基礎與原理", rx, textSize + textSize * i, paint);
        }
    }

    @Override
    protected void logic() {
        if(isScrool){
            // 超出屏幕長度
            if ((rx += 5) > getWidth()) {
                rx = 0 - paint.measureText("自定義View基礎與原理");
            }
            // 改變畫筆的顏色
            int r = random.nextInt(256);
            int g = random.nextInt(256);
            int b = random.nextInt(256);
            paint.setARGB(255, r, g, b);
        }
    }
}

運行結果:這裏寫圖片描述

源碼下載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章