天天看點

3.6 自定義View (3.6.1)

本文對應項目的碼雲位址: https://gitee.com/wanchuanxy/AndroidHeroesTest/tree/master/3/SystemWidget

Android給我們提供了豐富的元件庫來建立豐富的UI效果,同時也提供了非常友善的拓展方法。通過繼承Android的系統元件,我們可以非常友善地拓展現有功能,在系統元件的基礎上建立新的功能,甚至可以直接自定義一個控件,實作Android系統控件所沒有的功能。自定義控件作為Android中一個非常重要的功能,一直以來都被初學者認為是代表高手的象征。其實,自定義View并沒有想象中的那麼難,與其說是在自定義一個View,不如說是在設計一個圖形,隻有站在一個設計者的角度上,才可以更好地建立自定義View。我們不能機械地記憶所有繪圖的API,而是要讓這些API為你所用,結合現實中繪圖的方法,甚至是PhotoShop的技巧,才能設計出更好的自定義View。

适當地使用自定義View,可以豐富應用程式的體驗效果,但濫用自定義View則會帶來适得其反的效果。一個讓使用者覺得熟悉得控件,才是一個好的控件。如果一味追求炫酷的效果而建立自定義View,則會讓使用者覺得華而不實。而且,在系統原生控件可以實作功能的基礎上,系統也提供了主題、圖檔資源、各種風格來建立豐富的UI。這些控件都是經過了Android一代代版本疊代後的産物。即使這樣,在如今的版本中,依然存在不少Bug,更不要提我們自定義的View了。特别是現在Android ROM的多樣性,導緻Android的适配變得越來越複雜,很難保證自定義View在其他手機上也能達到你想要的效果。

當然,了解Android系統自定義View的過程,可以幫助我們了解系統的繪圖機智。同時,在适當的情況下也可以通過自定義View來幫助我們建立更佳靈活的布局。

在自定義View時,我們通常會去重寫onDraw()方法來揮着View的顯示内容。如果該View還需要使用wrap_content屬性,那麼還必須寫onMeasure()方法。另外,通過自定義attrs屬性,還可以設定新的屬性配置值。

在View中通常有以下一些比較重要的回調方法。

  • onFinishInflate():從XML加載元件後回調。
  • onSizeChanged():元件大小改變時回調。
  • onMeasure():回調該方法來進行測量。
  • onLayout():回調該方法來确定顯示的位置。
  • onTouchEvent():監聽到觸摸事件時回調。

當然,建立自定義View的時候,并不需要重寫所有的方法,隻需要重寫特定條件的回調方法即可。這也是Android控件架構靈活性的展現。

  

通常情況下,有以下三種方法來實作自定義的控件。

  • 對現有控件進行拓展
  • 通過組合來實作新的控件
  • 重寫View來實作全新的控件

3.6.1 對現有控件進行拓展

這是一個非常重要的自定義View方法,它可以在原生控件的基礎上進行拓展,增加新的功能、修改顯示的UI等。一般來說,我們可以再原生控件的基礎上進行拓展,增加新的功能、修改顯示的UI等。一般來說,我們可以在onDraw()方法中對原生控件行為進行拓展。

下面以一個TextView為例,來看看如何使用拓展原生控件的方法建立新的控件。比如想讓一個TextView的背景更佳豐富,給其多繪制幾層背景,如下圖所示。

3.6 自定義View (3.6.1)

我們先來分析一下如何實作這個效果,原生的TextView使用onDraw()方法繪制要顯示的文字。當繼承了系統的TextView之後,如果不重寫其onDraw()方法,則不會修改TextView的任何效果。可以認為在自定義的TextView中調用TextView類的onDraw()方法來繪制顯示的文字,代碼如下所示。

@Override
protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
}
           

程式調用super.onDraw(canvas)方法來實作原生控件的功能,但是在動用super.onDraw()方法之前和之後,我們都可以實作自己的邏輯,分别在系統繪制文字前後,完成自己的操作,即如下所示。

@Override
protected void onDraw(Canvas canvas) {
 //在調父類方法前,實作自己的邏輯,對TextView來說即是在繪制文本内容前
 super.onDraw(canvas);
 //在調父類方法後,實作自己的邏輯,對TextView來說即是在繪制文本内容後
}
           

以上就是通過改變控件的繪制行為建立自定義View的思路。有了上面的分析,我們就可以很輕松地實作上圖所示的自定義TextView了。我們在構造方法中完成必要對象的初始化工作,如初始化畫筆等,代碼如下所示。

mPaint1 = new Paint();
mPaint1.setColor(Color.BLUE);
mPaint1.setStyle(Paint.Style.FILL);
mPaint2 = new Paint();
mPaint2.setColor(Color.YELLOW);
mPaint2.setStyle(Paint.Style.FILL);
           

而代碼中最重要的部分則是在onDraw()方法中,為了改變原生的繪制行為,在系統調用super.onDraw(canvas)方法前,也就是在繪制文字之前,繪制兩個不同大小的矩形,形成一個重疊效果,再讓系統調用super.onDraw(canvas)方法,執行繪制文字的工作。這樣,我們就通過改變控件繪制行為,建立了一個新的控件,代碼如下所示。

//繪制外層矩形
canvas.drawRect(
 0,
 0,
 getMeasuredWidth(),
 getMeasuredHeight(),
 mPaint1);
//繪制内層矩形
canvas.drawRect(
 10,
 10,
 getMeasuredWidth() - 10,
 getMeasuredHeight() - 10,
 mPaint2);
canvas.save();
//繪制文字前平移10像素
canvas.translate(10,0);
//父類完成的方法,即繪制文本
super.onDraw(canvas);
canvas.restore();
           

此View全文(一些細節解析包含在注釋中了):

package com.imooc.systemwidget;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.TextView;

//思路:1.準備畫筆,2.繪制。完
public class MyTextView extends TextView {

    private Paint mPaint1, mPaint2;//聲明畫筆對象

    public MyTextView(Context context) {
        super(context);
        initView();
    }//三個重載構造函數

    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

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

    private void initView() {  //準備畫筆
        mPaint1 = new Paint();//準備畫筆1
        mPaint1.setColor(getResources().getColor(
                android.R.color.holo_blue_light));
        mPaint1.setStyle(Paint.Style.FILL);

        mPaint2 = new Paint();//準備畫筆2
        mPaint2.setColor(Color.YELLOW);
        mPaint2.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 繪制外層矩形  drawRect乃繪制矩形的方法
        // 參數:public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)
        // 按ctrl點選方法,自己看
        canvas.drawRect(
                0,
                0,
                getMeasuredWidth(),
                getMeasuredHeight(),
                mPaint1);
        // 繪制内層矩形
        canvas.drawRect(
                10,
                10,
                getMeasuredWidth() - 10,
                getMeasuredHeight() - 10,
                mPaint2);
        canvas.save();
        // 繪制文字前平移10像素
        // public void translate(float dx, float dy)
        //translate用于平移坐标系
        canvas.translate(10, 0);
        // 父類完成的方法,即繪制文本
        super.onDraw(canvas);
        canvas.restore();
        Log.d("MyTextView", "MyTextView_onDraw: ");
    }
}
           

下面再來看一個稍微複雜一點的TextView。在前面一個執行個體中,我們直接使用了Canvas對象來進行圖像的繪制,然後利用Android的繪圖機制,可以繪制出更複雜豐富的圖像。比如可以利用LinearGradient Shader 和Matrix來實作一個動态的文字閃動效果,程式運作效果如下圖所示。

3.6 自定義View (3.6.1)
  • 要想實作這個效果,可以充分利用Android中Paint對象的Shader渲染器。
  • 通過設定一個不斷變化的LinearGradient,并使用帶有該屬性的Paint對象來繪制要顯示的文字。
  • 首先,在onSizeChanged()方法中進行一些對象的初始化工作,并根據View的寬度設定一個LinearGradient漸變渲染器,代碼如下所示。 (注意細節的注釋)
@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        Log.d(TAG, "onSizeChanged: ");
        super.onSizeChanged(w, h, oldw, oldh);
        if (mViewWidth == 0) {
            mViewWidth = getMeasuredWidth();
            if (mViewWidth > 0) {
                mPaint = getPaint();
                mLinearGradient = new LinearGradient(  //gradient.梯度|LinearGradient是一個漸變渲染器!!!
                        0,
                        0,
                        mViewWidth,
                        0,
                        new int[]{
                                Color.BLUE, 0xffffffff,
                                Color.RED},
                        null,
                        Shader.TileMode.CLAMP);

//                 Create a shader that draws a linear gradient along a line.
//                 @param x0           The x-coordinate for the start of the gradient line
//                 @param y0           The y-coordinate for the start of the gradient line
//                 @param x1           The x-coordinate for the end of the gradient line
//                 @param y1           The y-coordinate for the end of the gradient line
//                 @param  colors      The colors to be distributed along the gradient line         用于設定漸變中的色彩
//                 @param  positions   May be null. The relative positions [0..1] of
//                 each corresponding color in the colors array. If this is null,
//                 the the colors are distributed evenly along the gradient line.顔色沿着梯度線均勻分布
//                 @param  tile        The Shader tiling mode
//
//    public LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[],
//                Shader.TileMode tile)

                mPaint.setShader(mLinearGradient);//設定渲染器
                mGradientMatrix = new Matrix();
//                Log.d(TAG, "onSizeChanged: ");
//                String i ="88888888888";
            }
        }
    }
           

其中最關鍵的就是使用getPaint()方法擷取目前繪制TextView的Paint對象,并給這個Paint對象設定原生TextView沒有的LinearGradient屬性。最後,在onDraw()方法中,通過矩形的方式來不斷平移漸變效果,進而在繪制文字時,産生動态的閃動效果,代碼如下所示。 (注意細節的注釋)

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d(TAG, "onDraw:000000 "+i);
        if (mGradientMatrix != null) {
            mTranslate += mViewWidth / 5;
//            Log.d(TAG, "onDraw: ");
            if (mTranslate > 2 * mViewWidth) {
                mTranslate = -mViewWidth;
            }
            mGradientMatrix.setTranslate(mTranslate, 0);//平移矩陣
            mLinearGradient.setLocalMatrix(mGradientMatrix);//通過設定平移的矩陣來平移漸變效果
            postInvalidateDelayed(100);//延時0.1s
//            Log.d(TAG, "onDraw2222: ");
        }
    }
}
           

onDraw()思路簡析:

3.6 自定義View (3.6.1)

本View.java全文:

package com.imooc.systemwidget;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.TextView;

public class ShineTextView extends TextView {

    private LinearGradient mLinearGradient;
    private Matrix mGradientMatrix;
    private Paint mPaint;
    private int mViewWidth = 0;
    private int mTranslate = 0;
    private final String TAG = "ShineTextView";
    private String i = "0" ;

    public ShineTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        Log.d(TAG, "onSizeChanged: ");
        super.onSizeChanged(w, h, oldw, oldh);
        if (mViewWidth == 0) {
            mViewWidth = getMeasuredWidth();
            if (mViewWidth > 0) {
                mPaint = getPaint();
                mLinearGradient = new LinearGradient(  //gradient.梯度|LinearGradient是一個漸變渲染器!!!
                        0,
                        0,
                        mViewWidth,
                        0,
                        new int[]{
                                Color.BLUE, 0xffffffff,
                                Color.RED},
                        null,
                        Shader.TileMode.CLAMP);

//                 Create a shader that draws a linear gradient along a line.
//                 @param x0           The x-coordinate for the start of the gradient line
//                 @param y0           The y-coordinate for the start of the gradient line
//                 @param x1           The x-coordinate for the end of the gradient line
//                 @param y1           The y-coordinate for the end of the gradient line
//                 @param  colors      The colors to be distributed along the gradient line         用于設定漸變中的色彩
//                 @param  positions   May be null. The relative positions [0..1] of
//                 each corresponding color in the colors array. If this is null,
//                 the the colors are distributed evenly along the gradient line.顔色沿着梯度線均勻分布
//                 @param  tile        The Shader tiling mode
//
//    public LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[],
//                Shader.TileMode tile)

                mPaint.setShader(mLinearGradient);//設定渲染器
                mGradientMatrix = new Matrix();
//                Log.d(TAG, "onSizeChanged: ");
//                String i ="88888888888";
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d(TAG, "onDraw:000000 "+i);
        if (mGradientMatrix != null) {
            mTranslate += mViewWidth / 5;
//            Log.d(TAG, "onDraw: ");
            if (mTranslate > 2 * mViewWidth) {
                mTranslate = -mViewWidth;
            }
            mGradientMatrix.setTranslate(mTranslate, 0);//平移矩陣
            mLinearGradient.setLocalMatrix(mGradientMatrix);//通過設定平移的矩陣來平移漸變效果
            postInvalidateDelayed(100);//延時0.1s
//            Log.d(TAG, "onDraw2222: ");
        }
    }
}

/*
postInvalidateDelayed(100);線程重新整理,0.1s一刷,每刷調用onDraw一次****反複不停地調用onDraw()
07-16 07:19:59.949 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:19:59.949 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.065 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.065 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.181 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.181 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.298 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.298 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.414 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.414 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.532 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.532 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.648 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.649 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.764 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.764 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.881 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.881 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:00.997 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:00.997 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
07-16 07:20:01.115 28857-28857/com.example.helloworld D/ShineTextView: onDraw:000000
07-16 07:20:01.115 28857-28857/com.example.helloworld D/ShineTextView: onDraw:
 */
           

内容參考自

Blankj

繼續閱讀