Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android知識梳理之自定義View

Android知識梳理之自定義View

編輯:關於Android編程

雖然android本身給我們提供了形形色色的控件,基本能夠滿足日常開發的需求,但是面對日益同質化的app界面,和不同的業務需求.我們可能就需要自定義一些View來獲得比較好的效果.自定義View是android開發者走向高級開發工程師必須要走的一關.

一,構造函數:

當我們創建一個類去繼承View的時候,會要求我們至少去實現一個構造函數. public MyView(Context context) 該構造函數是直接在代碼裡面進行創建控件的時候調用.如我們創建一個MyView myView=new MyView(this)的時候將會調用該函數.
public MyView(Context context, AttributeSet attrs)這個是在xml創建但是沒有指定style的時候被調用.多了一個AttributeSet類型的參數,在通過布局文件xml創建一個view時,會把XML內的參數通過AttributeSet帶入到View內。
public MyView(Context context, AttributeSet attrs, int defStyleAttr)構造函數中第三個參數是默認的Style,這裡的默認的Style是指它在當前Application或Activity所用的Theme中的默認Style,且只有在明確調用的時候才會生效.
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)該構造函數是在api21的時候才添加上的,暫不考慮.
注意:即使你在View中使用了Style這個屬性也不會調用三個參數的構造函數,所調用的依舊是兩個參數的構造函數。 想要深入了解的詳情可以查看這篇文章:http://blog.csdn.net/yuzhouxiang/article/details/6958017 android view構造函數研究
一般我們構造函數可以寫成這樣:
 public MyView(Context context) {
        this(context, null);
    }
    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
二.自定義命名空間: 當我們自定義View的時候許多的屬性我們當然不希望被寫死.例如我自定義了一個圓.這個圓的顏色我更希望不在自定義控件裡面寫死,而是在使用的時候在布局文件中進 行指定這個顏色的時候.我們就需要用到自定義命名空間來對自定義View的屬性進行設置了. 步驟1:首先在Values文件夾裡面新建attrs文件. \ 步驟2.編寫attrs文件.attrs有兩種寫法.
( 1.)針對於單個View自己去定義不同的屬性.
例如:


    
    
        
        
    
(2.)如果是有公共的屬性部分,可以將屬性包含在公共屬性部分裡面.也就是說公共屬性可以被多個自定義控件屬性樣式使用.
例如:


    
    
    
    
        
    
所有的format類型
reference 引用
color 顏色
boolean 布爾值
dimension 尺寸值
float 浮點值
integer 整型值
string 字符串
enum 枚舉值 步驟3:在布局文件中隊自定義的屬性進行使用。
打開布局文件我們可以看到有很多的以xmlns開頭的字段。其實這個就是XML name space 的縮寫。我們仿照系統定義好的自己來定義一個命名空間。
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:上面說過了是XML name space 的縮寫。
app :是命名空間的名稱可隨意書寫
上方是在android studio裡面萬能的書寫方式,在eclipse裡面一般是這樣寫xmlns:app="http://schemas.android.com/apk/res/com.dapeng.viewdemo"
com.dapeng.viewdemo為本應用的包名.
步驟4.在自定義View中將我們定義好的屬性拿到.
通過context.obtainStyledAttributes將構造函數中的attrs進行解析出來,就可以拿到相對應的屬性.
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView);
mColor = typedArray.getColor(R.styleable.MyView_roundColor, 0XFF00FF00);
需要注意的一點是:在我們獲取尺寸的時候有三個函數進行使用,我們來看看他們之間的區別.
getDimension()是基於當前DisplayMetrics進行轉換,獲取指定資源id對應的尺寸。文檔裡並沒說這裡返回的就是像素,要注意這個函數的返回值是float,像素肯定 是int。
getDimensionPixelSize()與getDimension()功能類似,不同的是將結果轉換為int,並且小數部分四捨五入。
getDimensionPixelOffset()與getDimension()功能類似,不同的是將結果轉換為int,並且偏移轉換(offset conversion,函數命名中的offset是這個意思)是直接截 斷小數位,即取整(其實就是把float強制轉化為int,注意不是四捨五入哦)。
由此可見,這三個函數返回的都是絕對尺寸,而不是相對尺寸(dp\sp等)。如果getDimension()返回結果是20.5f,那麼getDimensionPixelSize()返回結果就是 21,getDimensionPixelOffset()返回結果就是20。

理論知識講的有點多,可能有點空洞,下面通過一個小的例子,來測試一下我們的命名空間是否可以正常使用.
例子:我們自定義一個View,這個View的形狀是一個圓形,並且我們不希望將圓的顏色寫死,可以在布局文件中進行設置.
其他的我們都先不管,只是測試一下自定義命名空間.
步驟1.創建一個View繼承自View.並且重寫它的構造函數
public class MyView extends View {
    public MyView(Context context) {
        this(context, null);
    }
    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}
步驟2:在Values文件夾下面創建一個attrs的文件,寫上自定義的屬性.


    
    
        
        
        
    
步驟3:在布局文件中進行引用自定義命名空間.這裡給設置的顏色是橘黃色.


    
步驟4:在自定義View中設置我們自定義的屬性.
public class MyView extends View {
    private int mColor;
    private Paint mP;
    private float mRadius;
    public MyView(Context context) {
        this(context, null);
    }
    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //拿到自定義屬性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView);
        mColor = typedArray.getColor(R.styleable.MyView_roundColor, 0XFF00FF00);
        mRadius = typedArray.getDimension(R.styleable.MyView_radius, 50);
        //回收資源
        typedArray.recycle();
        //創建畫筆
        mP = new Paint();
        //設置畫筆顏色
        mP.setColor(mColor);
        //設置抗鋸齒
        mP.setAntiAlias(true);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //畫圓
        canvas.drawCircle(mRadius, mRadius, mRadius, mP);
    }
}
Ok代碼書寫完畢,我們來看看實現的效果是怎麼樣的.
\如果將顏色設置為藍色就是如下效果:
  \

當然,這裡只是實現了自定義控件的一小部分功能,接著我們來看看一個問題:
我們將我們自定義控件的background設置為紅色來看看效果.這裡控件都是設置包裹內容的.


    
效果如下圖所示: \

看到這個效果可能會有點驚奇了,明明我設置的是包裹內容的,為什麼控件確實填充了父窗體?帶著這樣的疑問我們接下來學習,自定義控件的另外一個非常重要的函數:onmeasure();
onmeuse()方法:測量自己的大小,為正式布局提供建議。(注意,只是建議,至於用不用,要看onLayout);
主要的作用就是處理自定義VIewgroup的時候是wrap_content的時候該ViewGrop的大小.
定義:如果layout_widht和layout_height是match_parent或具體的xxxdp,那就非常簡單了,直接調用setMeasuredDimension()方 法,設置ViewGroup的寬高即可.But如果是wrap_content,就比較麻煩了,如果不重寫onMeasure()方法,系統則會不知道該默認多 大尺寸,就會默認填充整個父布局,所以,重寫onMeasure()方法的目的,就是為了能夠給 View 一個wrap_content屬性下的默認大 小。
調用此方法會傳進來的兩個參數:int widthMeasureSpec,int heightMeasureSpec.他們是父類傳遞過來給當前view的一個建議值, 即把當前view的尺寸設置為寬widthMeasureSpec,高heightMeasureSpec雖然表面上看起來他們是int類型的數字,其實他們是由 mode+size兩部分組成的。widthMeasureSpec和heightMeasureSpec轉化成二進制數字表示,他們都是30位的。前兩位代表mode(測量模 式),後面28位才是他們的實際數值(size)。
MeasureSpec.getMode()獲取模式
MeasureSpec.getSize()獲取尺寸
mode的值有三種為:
EXACTLY:表示設置了精確的值,一般當childView設置其寬、高為精確值(也就是我們在布局文件中設定的值如50dp)、match_parent時,ViewGroup會將其設置為EXACTLY;

AT_MOST:表示子布局被限制在一個最大值內,一般當childView設置其寬、高為wrap_content時,ViewGroup會將其設置為AT_MOST;

UNSPECIFIED:表示子布局想要多大就多大,一般出現在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此種模式比較少見。

我們需要判斷當布局文件中設置控件為包裹內容的時候,控件的大小的值就可以了.因此重寫onmeasure()方法如下:
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //獲取測量的模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //獲取測量的值
        int withSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //設置控件的大小
        setMeasuredDimension(widthMode == MeasureSpec.AT_MOST ? (int) mRadius * 2 : withSize, heightMode == MeasureSpec.AT_MOST ? (int) mRadius * 2 : heightSize);
    }
設置成功以後,直接將工程運行起來就可以看到效果了: \

上面的例子都是演示的畫圓,如果想畫其他的形狀應該怎麼辦呢?我們需要通過重寫onDraw() 方法對控件重寫進行繪制就可以了.
Ondraw().
draw就是畫的意思從字面意思我們也可以知道.通過重寫該方法我們可以對繪制出相關的控件.那麼繪制的時候在是在什麼上面進行繪制呢?我們 先來重寫ondraw()看看:
@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
       
    }


我們可以看到通過重寫ondraw()會傳過來一個Canvas類,這個類實際上就是一塊兒畫布,我們可以創建畫筆在上面進行繪圖.

Canvas的使用.

這個類相當於一個畫布,你可以在裡面畫很多東西;

我們可以把這個Canvas理解成系統提供給我們的一塊內存區域(但實際上它只是一套畫圖的API,真正的內存是下面的 Bitmap),而且它還提供了一整套對這個內存區域進行操作的方法,所有的這些操作都是畫圖API。也就是說在這種方式

下我們已經能一筆一劃或者使用畫筆來畫我們所需要的東西了,要畫什麼要顯示什麼都由我們自己控制。這種方式根據環 境還分為兩種:一種就是使用普通View的canvas畫圖,還有一種就是使用專門的SurfaceView的canvas來畫圖。兩種的 主要是區別就是可以在SurfaceView中定義一個專門的線程來完成畫圖工作,應用程序不需要等待View的刷圖,提高性 能。前面一種適合處理量比較小,幀率比較小的動畫,比如說簡單的View樣式或者是象棋游戲之類的;而後一種主要用 在游戲,高品質動畫方面的畫圖。

Canvas可以繪制的對象有:弧線(arcs)、填充顏色(argb和color)、 Bitmap、圓(circle和oval)、點(point)、線(line)、矩形 (Rect)、圖片(Picture)、圓角矩形 (RoundRect)、文本(text)、頂點(Vertices)、路徑(path)。通過組合這些對象我們可以畫出一 些簡單有趣的界面出來,但是光有這些功能還是不夠的,如果我要畫一個儀表盤(數字圍繞顯示在一個圓圈中)呢? 幸好Android 還提供了一些對Canvas位置轉換的方法:rorate、scale、translate、skew(扭曲)等,而且它允許你通過獲得它的轉換矩陣對象 (getMatrix方法) 直接操作它。這些操作就像是雖然你的筆還是原來的地方畫,但是畫紙旋轉或者移動了,所以你畫的東西的方 位就產生變化。為了方便一些轉換操作,Canvas 還提供了保存和回滾屬性的方法(save和restore),比如你可以先保存目前畫紙 的位置(save),然後旋轉90度,向下移動100像素後畫一些圖形,畫完後調用restore方法返回到剛才保存的位置. 畫一些比較常見的幾何圖形: 畫圓:canvas.drawCircle()
 canvas.drawCircle(100, 100, 90, paint);   
畫弧形:canvas.drawArc();
 //繪制弧線區域   
    //先要繪制矩形                                                                                                                              
    RectF rect = new RectF(0, 0, 100, 100);   
                                                                                                                                  
    canvas.drawArc(rect, //弧線所使用的矩形區域大小   
            270,  //開始角度   
            90, //掃過的角度   
            true, //是否使用中心   
            paint); //畫筆  
顏色填充:canvas.drawColor();
 canvas.drawColor(Color.BLUE);   
畫一條線:canvas.drawLine()
canvas.drawLine(10,//x起點位置
                10, //y起點位置
                100, //x終點位置
                100, //y終點位置
                paint); //畫筆
畫橢圓:
                                                                                                                             
    //定義一個矩形區域   
    RectF oval = new RectF(0,0,200,300);   
    //矩形區域內切橢圓   
    canvas.drawOval(oval, paint);   
畫帶有弧度的文字:canvas.drawPosText();
//按照既定點 繪制文本內容   
    canvas.drawPosText("Android", new float[]{   
            10,10, //第一個字母在坐標10,10   
            20,20, //第二個字母在坐標20,20   
            30,30, //....   
            40,40,   
            50,50,   
            60,60,   
            70,70,   
    }, paint);   
畫矩形:canvas.drawRect();
 RectF rect = new RectF(50, 50, 200, 200);   
                                                                                                                                  
        canvas.drawRect(rect, paint);  
畫帶有弧度的矩形:canvas.drawRoundRect();
  RectF rect = new RectF(50, 50, 200, 200);   
                                                                                                                                  
    canvas.drawRoundRect(rect,   
                        30, //x軸的半徑   
                        30, //y軸的半徑   
                        paint);   
畫封閉的圖形:
  Path path = new Path(); //定義一條路徑   
    path.moveTo(10, 10); //移動到 坐標10,10   
    path.lineTo(50, 60);   
    path.lineTo(200,80);   
    path.lineTo(10, 10);   
                                                                                                                                  
    canvas.drawPath(path, paint);   
畫文字跟隨一條線:
  Path path = new Path(); //定義一條路徑   
            path.moveTo(10, 10); //移動到 坐標10,10   
            path.lineTo(50, 60);   
            path.lineTo(200,80);   
            path.lineTo(10, 10);                                                                                                                                 
            canvas.drawTextOnPath("Android", path, 10, 10, paint);
畫圖片:
drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
canvas的一些常規的方法:
 canvas.rotate(360 / count,//旋轉的角度 
                        0f, x軸的坐標
                        0f); //旋轉畫紙
 canvas.translate(200, 200); //將位置移動畫紙的坐標點到x為200,y為200
canvas.save();              //保存畫布的狀態
canvas.restore();           //取出保存的狀態 

canvas.save();和canvas.restore();是兩個相互匹配出現的,作用是用來保存畫布的狀態和取出保存的狀態的。這裡稍微解釋一下,

當我們對畫布進行旋轉,縮放,平移等操作的時候其實我們是想對特定的元素進行操作,比如圖片,一個矩形等,但是當你用canvas的方法來進行這些操作的時候,其實是對整個畫布進行了操作,那麼之後在畫布上的元素都會受到影響,所以我們在操作之前調用canvas.save()來保存畫布當前的狀態,當操作之後取出之前保存過的狀態,這樣就不會對其他的元素進行影響.

 

畫筆Paint

  從上面列舉的幾個Canvas.drawXxx()的方法看到,其中都有一個類型為paint的參數,可以把它理解為一個"畫筆",通過這個畫筆,在Canvas這張畫布上作畫。它位於"android.graphics.Paint"包下,主要用於設置繪圖風格,包括畫筆顏色、畫筆粗細、填充風格等。

  Paint中提供了大量設置繪圖風格的方法,這裡僅列出一些常用的:

  • setARGB(int a,int r,int g,int b):設置ARGB顏色。
  • setColor(int color):設置顏色。
  • setAlpha(int a):設置透明度。
  • setPathEffect(PathEffect effect):設置繪制路徑時的路徑效果。
  • setShader(Shader shader):設置Paint的填充效果。
  • setAntiAlias(boolean aa):設置是否抗鋸齒。
  • setStrokeWidth(float width):設置Paint的筆觸寬度。
  • setStyle(Paint.Style style):設置Paint的填充風格。
  • setTextSize(float textSize):設置繪制文本時的文字大小。

 

invalidate()和postInvalidate()的區別.

通過上面的講解,我在自定義一個靜態的View已經是一件非常容易的事情了,但是我們使用的自定義的View有很多是需要根據一個變量去不斷繪制的,這個時候就引出了新的函數invalidate()和postinvalidate(),使用此函數可以是的ondraw()不斷的去執行從而達到不斷繪制的效果.

Android中實現view的更新有兩組方法,一組是invalidate,另一組是postInvalidate,其中前者是在UI線程自身中使用,而後者在非UI線程中使用。

接下來通過一個稍微綜合一點的例子來對自定義View做一個總結:

先來看看實現的效果:

\

我們先來分析一下,這個效果實際上就是外面是不斷的去繪制一個扇形,然後中間蓋了一個小的圓:

\\

好了,接下來我們來講一講實現的步驟:

步驟一:首先定義attrs文件:

 



    
        
        
        
        
        
        
    
步驟二:編寫自定義View:

 

 

public class ProgressView extends View {
    private float mArcRadius;
    private float mSmallRoundRadius;
    private int mArcColor;
    private int mSmallRoundColor;
    private Paint mRoundpaint;
    private Paint mArcpaint;
    private float sweepAngle;
    private int mTextColor;
    private Paint mTextPaint;
    private int mTextSize;
    public ProgressView(Context context) {
        this(context, null);
    }
    public ProgressView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
        //扇形半徑
        mArcRadius = array.getDimension(R.styleable.ProgressView_arcRadius, 50);
        //小圓半徑
        mSmallRoundRadius = array.getDimension(R.styleable.ProgressView_smallRoundRadius, 50);
        //扇形顏色
        mArcColor = array.getColor(R.styleable.ProgressView_arcColor, 0XFF00FF00);
        //小圓顏色
        mSmallRoundColor = array.getColor(R.styleable.ProgressView_smallRoundColor, 0XFF00FF00);
        //百分比字體顏色
        mTextColor = array.getColor(R.styleable.ProgressView_textColor, 0XFF00FF00);
        //百分比字體大小
        mTextSize = array.getDimensionPixelSize(R.styleable.ProgressView_textSize, 15);
        //釋放資源
        array.recycle();
        //畫圓的畫筆
        mRoundpaint = new Paint();
        mRoundpaint.setColor(mSmallRoundColor);
        mRoundpaint.setAntiAlias(true);
        //畫扇形的畫筆
        mArcpaint = new Paint();
        mArcpaint.setColor(mArcColor);
        mArcpaint.setAntiAlias(true);
        //繪制中間文字部分的畫筆文本
        mTextPaint = new Paint();
        mTextPaint.setColor(mTextColor);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextSize(mTextSize);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //設置View的大小
        int withMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int withSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightsize = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(withMode == MeasureSpec.AT_MOST ? (int) mArcRadius * 2 : withSize, heightMode == MeasureSpec.AT_MOST ? (int) mArcRadius * 2 : heightsize);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        //畫圓弧
        RectF rect = new RectF(0, 0, (int) mArcRadius * 2, (int) mArcRadius * 2);
        canvas.drawArc(rect, 270, (float) (sweepAngle * 3.6), true, mArcpaint);
        //畫小圓
        canvas.drawCircle(mArcRadius, mArcRadius, mSmallRoundRadius, mRoundpaint);
        String text = (int) (sweepAngle) + "%";
        float textLength = mTextPaint.measureText(text);
        //把文本畫在圓心居中
        canvas.drawText(text, mArcRadius - textLength / 2, mArcRadius, mTextPaint);
        super.onDraw(canvas);
    }
    //提供一個給外界的方法可以不斷的去設置扇形的弧度
    public void percent(float sweepAngle) {
        if (sweepAngle <= 100) {
            this.sweepAngle = sweepAngle;
            //刷新界面
            postInvalidate();
        }
    }
}
步驟三:在布局文件中進行使用:

 

 



    
步驟四:在需要用到的地方模擬數據去使用自定義的View.
public class SecondActivity extends AppCompatActivity {
    private int mTotalProgress;
    private int mCurrentProgress;
    private ProgressView mPv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        mPv = (ProgressView) findViewById(R.id.pv);
        initVariable();
        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mCurrentProgress=0;
                new Thread(new ProgressRunable()).start();
            }
        });
    }
    private void initVariable() {
        mTotalProgress = 100;
        mCurrentProgress = 0;
    }
    class ProgressRunable implements Runnable {
        @Override
        public void run() {
            while (mCurrentProgress < mTotalProgress) {
                mCurrentProgress += 1;
                mPv.percent((float) mCurrentProgress);
                try {
                    Thread.sleep(50);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

至此大功告成.


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