[Android] View 的三種自定義方式:擴(kuò)展,組合,重寫

前言


下面文章中涉及到的代碼全部可以在我的github上得到:https://github.com/celesteshire/TestView

Android 中已經(jīng)提供了很多的 View 給我們使用,但是有時(shí)候因?yàn)樘厥庑枨蟮脑颍@些 View 并不能滿足需求,這個(gè)時(shí)候就需要自己來設(shè)計(jì) View 。通常在自定義 View 的時(shí)候需要重寫 onDraw() 方法來繪制需要顯示的內(nèi)容,如果這個(gè) View 需要使用 wrap_content 屬性,還需要重寫 onMeasure() 方法,對(duì)于前言不明白的可以看看我的另一篇文章:

在 View 中有以下一些常用的方法:

  • onFinishInflate() -- 從 XML 加載組件后的回調(diào)
  • onSizeChanged() -- 組件大小發(fā)生變化時(shí)的回調(diào)
  • onMeasure() -- 回調(diào)此方法對(duì)組件大小進(jìn)行測(cè)量
  • onLayout() -- 回調(diào)該方法來確定顯示的位置
  • onTouchEvent() -- 監(jiān)聽到觸摸事件時(shí)的回調(diào)

并不需要重寫以上所有的方法,根據(jù)自己的需求重寫其中的部分方法即可,通常有三種方法來實(shí)現(xiàn)自定義控件。

  • 擴(kuò)展 -- 對(duì)現(xiàn)有控件進(jìn)行擴(kuò)展
  • 組合 -- 將不同的控件組合在一起形成新的空間
  • 重寫 -- 通過重寫來實(shí)現(xiàn)全新的控件

擴(kuò)展


這是自定義 View 中重點(diǎn)方法之一,他可以在原生控件的基礎(chǔ)上進(jìn)行擴(kuò)展,增加功能,修改 UI 顯示效果等,下面以一個(gè) TextView 為例子,看看如何對(duì)他進(jìn)行擴(kuò)展,比如如何讓一個(gè) TextView 的背景更加豐富,字體絢爛等 。

先看看普通的TextView 。

原始的TextView

要修改他的顯示效果,應(yīng)該重寫它的 onDraw() 方法。

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

程序調(diào)用 super.onDraw(canvas) 方法來實(shí)現(xiàn)原生控件的繪制,要想在這基礎(chǔ)上進(jìn)行實(shí)現(xiàn)自己的邏輯,可以在方法的前后添加代碼,在方法前添加的,就是繪制在原生 TextView 的底層,在方法后添加的,就是繪制在原生 TextView 的上層。

我用一個(gè)實(shí)例來表現(xiàn)這個(gè)層疊關(guān)系,這是一個(gè)自定義的 TextView,我重寫了 onDraw() 方法。

public class OneTextView extends TextView {
  private Paint mPaint;

  public OneTextView(Context context, AttributeSet attrs) {
    super(context, attrs);
    mPaint = new Paint();
  }

  @Override
  protected void onDraw(Canvas canvas) {
    //這部分是在原生控件繪制之前進(jìn)行繪制的,位于原生控件繪制區(qū)域底層,不會(huì)遮擋其他內(nèi)容

    //第一個(gè)矩形,大小跟此控件一樣大,位于底層
    mPaint.setColor(Color.BLUE);
    canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);

    //第二個(gè)矩形,稍微小一點(diǎn),但是會(huì)遮擋第一個(gè)矩形
    mPaint.setColor(Color.RED);
    canvas.drawRect(10, 10, getMeasuredWidth() - 10, getMeasuredHeight() - 10, mPaint);

    //原生控件開始繪制,會(huì)遮擋上面代碼繪制的內(nèi)容。
    super.onDraw(canvas);

    //第三個(gè)矩形,這是在原生控件繪制完畢后進(jìn)行繪制的,位于原生控制的上層,會(huì)遮擋所有之前繪制的內(nèi)容
    mPaint.setColor(Color.GREEN);
    canvas.drawRect(20, 20,200, 100, mPaint);
  }
}

看看最后的效果

層疊效果

可以看到最底層的藍(lán)色矩形,第二層的紅色矩形,第三層的文字,第四層的綠色矩形(從底層往上數(shù)),這下可以直觀的理解在繪制過程中的層疊覆蓋關(guān)系了吧,了解這個(gè)對(duì)于以后自定義控件的時(shí)候很有幫助。

下面我進(jìn)行一個(gè)比較復(fù)雜的自定義 TextView 。字體能夠?qū)崿F(xiàn)漸變效果,這里我會(huì)用到 LinearGradient 以及 Matrix ,如果不了解可以查閱:
LinearGradient
Matrix

public class TwoTextView extends TextView {
  int mViewWidth;
  LinearGradient mLinearGradient;
  Matrix mMatrix;
  Paint mPaint;
  int mTranslate=0;

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

  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    //在 onSizeChanged 方法中獲取到寬度,并對(duì)各個(gè)類進(jìn)行初始化
    if (mViewWidth == 0) {
      mViewWidth = getMeasuredWidth();

      if (mViewWidth > 0) {
        //得到 父類 TextView 中寫字的那支筆。。。
        mPaint = getPaint();
        //初始化線性渲染器 不了解的請(qǐng)看上面連接
        mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, 
        new int[]{Color.BLUE, Color.YELLOW, Color.RED, Color.GREEN}, null, Shader.TileMode.CLAMP);
        //把渲染器給筆套上
        mPaint.setShader(mLinearGradient);
        //初始化 Matrix
        mMatrix = new Matrix();

      }
    }
  }

  @Override
  protected void onDraw(Canvas canvas) {
    //先讓父類方法執(zhí)行,由于上面我們給父類的 Paint 套上了渲染器,所以這里出現(xiàn)的文字已經(jīng)是彩色的了
    super.onDraw(canvas);
    
    if (mMatrix != null) {
      //利用 Matrix 的平移動(dòng)作實(shí)現(xiàn)霓虹燈的效果,這里是每次滾動(dòng)1/10
      mTranslate += mViewWidth / 10;
      //如果滾出了控件邊界,就要拉回來重置開頭,這里重置到了屏幕左邊的空間
      if (mTranslate >  mViewWidth) {
        mTranslate = -mViewWidth/2;
      }
    //設(shè)置平移距離
    mMatrix.setTranslate(mTranslate, 0);  
    //平移效果生效
    mLinearGradient.setLocalMatrix(mMatrix);
    //延遲 100 毫秒再次刷新 View 也就是再次執(zhí)行本 onDraw 方法
    postInvalidateDelayed(100);

    }
  }
}
動(dòng)態(tài)效果

炫酷吧,重點(diǎn)其實(shí)在于如何利用好 LinearGradient 和 Matrix 還有 Paint , Canvas 也很重要,這些在自定義 View 中都是經(jīng)常用到的。

組合


有的時(shí)候其實(shí)可以使用幾個(gè)基本控件組合在一起,形成一個(gè)新的控件。這種方式通常都需要繼承一個(gè)合適的 ViewGroup ,再給他添加指定功能的控件,形成新的空間。通過這種方式創(chuàng)建的控件我們還可以給他指定一些可配置的屬性,增強(qiáng)它的可操控性,下面以一個(gè)標(biāo)題欄為例子來說明如何創(chuàng)建組合控件。

一個(gè)標(biāo)題欄通常貫穿了整個(gè)應(yīng)用程序的大部分界面,大部分布局是左右兩個(gè) Button ,中間一個(gè) TextView ,如果每個(gè)頁(yè)面都去寫一次,那未免太過繁瑣了,我把它抽象出來,形成一個(gè)通用的標(biāo)題欄,并且可以任意更改按鈕文字和標(biāo)題文字等屬性,還要提供接口給調(diào)用者操作點(diǎn)擊事件。

所以,我們需要對(duì)這個(gè)標(biāo)題欄創(chuàng)建一些自定義屬性,只需要在 res 資源目錄的 values 目錄下創(chuàng)建一個(gè) attrs.xml 文件,并添加代碼即可,看看下面的代碼吧。

<?xml version="1.0" encoding="utf-8"?>
<resources>

  <declare-styleable name="TopBar">
    <attr name="titleText" format="string"/>
    <attr name="titleTextSize" format="dimension"/>
    <attr name="titleColor" format="color"/>
    <attr name="leftText" format="string"/>
    <attr name="leftTextColor" format="color"/>
    <attr name="leftBackground" format="color|reference"/>
    <attr name="rightText" format="string"/>
    <attr name="rightTextColor" format="color"/>
    <attr name="rightBackground" format="color|reference"/>
  </declare-styleable>

</resources>

通過 declare-styleable 標(biāo)簽表示使用自定義屬性,通過 name 屬性來表示引用的名稱,通過 format 來確定屬性的類型,這里定義的東西就是我們平時(shí)寫布局文件 XML 的時(shí)候調(diào)用的屬性啦。這里分別設(shè)置了以下屬性:

屬性 解釋
titleText 標(biāo)題文本
titleColor 標(biāo)題文本顏色
titleTextSize 標(biāo)題文本字體大小
leftText 左邊按鈕文本
leftTextColor 左邊按鈕文本顏色
leftBackground 左邊按鈕背景
rightText 右邊按鈕文本
rightTextColor 右邊按鈕文本顏色
rightBackground 右邊按鈕背景

上面是對(duì)這個(gè)自定義標(biāo)題欄的diy參數(shù),那么在布局 XML 文件中應(yīng)該如何使用?

<com.shire.testview.MyTopBar
  xmlns:custom="http://schemas.android.com/apk/res-auto"
  android:id="@+id/mytopbar"
  android:layout_width="match_parent"
  android:layout_height="45dp"
  android:background="#3F51B5"
  custom:leftBackground="#ffffff"
  custom:leftText="Back"
  custom:leftTextColor="#000000"
  custom:rightBackground="#ffffff"
  custom:rightText="+"
  custom:rightTextColor="#000000"
  custom:titleColor="#ffffff"
  custom:titleText="test"
  custom:titleTextSize="8sp"
>

以上,注意第二行加入了一個(gè)命名空間 “http://schemas.android.com/apk/res-auto”,這個(gè)命名空間是使用自定義參數(shù)的時(shí)候使用的,這里我命名為了 custom 。

完成 xml 文件的編寫后,來看看這個(gè)自定義標(biāo)題欄的類應(yīng)該怎么寫,這個(gè)自定義的標(biāo)題欄需要繼承一個(gè) ViewGroup 來包裹多個(gè)普通 View , 這里繼承的是 RelativeLayout。

public class MyTopBar extends RelativeLayout {

  //定義了各個(gè)控件的屬性變量
  String titleText, leftText, rightText;
  int titleColor, leftTextColor, rightTextColor;
  float titleTextSize;
  Drawable leftBackground, rightBackground;
  Button rightButton, leftButton;
  TextView titleTextView;
  MyTopBarClickListener myTopBarClickListener;

  public MyTopBar(Context context, AttributeSet attrs) {
    super(context, attrs);
    //通過 TypedArray 可以從 XML 文件中取出相應(yīng)的屬性
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TopBar);

    //使用R.styleable.xxxxx 就可以取出對(duì)應(yīng)數(shù)據(jù)類型的具體數(shù)據(jù),第二個(gè)參數(shù)是默認(rèn)值 如果取值為空就會(huì)使用默認(rèn)值
    titleText = typedArray.getString(R.styleable.TopBar_titleText);
    titleTextSize = typedArray.getDimension(R.styleable.TopBar_titleTextSize, 20);
    titleColor = typedArray.getColor(R.styleable.TopBar_titleColor, 0);

    leftText = typedArray.getString(R.styleable.TopBar_leftText);
    leftTextColor = typedArray.getColor(R.styleable.TopBar_leftTextColor, 0);
    leftBackground = typedArray.getDrawable(R.styleable.TopBar_leftBackground);

    rightText = typedArray.getString(R.styleable.TopBar_rightText);
    rightTextColor = typedArray.getColor(R.styleable.TopBar_rightTextColor, 0);
    rightBackground = typedArray.getDrawable(R.styleable.TopBar_rightBackground);

    typedArray.recycle();
  }

通過上面部分的代碼,我們?nèi)〕隽嗽?XML 文件中進(jìn)行設(shè)置的參數(shù),接下來應(yīng)該是構(gòu)造子控件,賦予參數(shù),并添加到此 ViewGroup 中。

  public void initView() {
    //創(chuàng)建三個(gè)控件對(duì)象
    rightButton = new Button(MainActivity.mContext);
    leftButton = new Button(MainActivity.mContext);
    titleTextView = new TextView(MainActivity.mContext);

    //將之前得到的相應(yīng)屬性賦給相應(yīng)的對(duì)象

    rightButton.setText(rightText);
    rightButton.setTextColor(rightTextColor);
    rightButton.setBackground(rightBackground);

    leftButton.setText(leftText);
    leftButton.setTextColor(leftTextColor);
    leftButton.setBackground(leftBackground);

    titleTextView.setText(titleText);
    titleTextView.setTextSize(titleTextSize);
    titleTextView.setTextColor(titleColor);

    //對(duì)各個(gè)子控件設(shè)置布局元素,并將它添加到此 ViewGroup 中。
    LayoutParams leftParams =
        new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
    leftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
    addView(leftButton, leftParams);

    LayoutParams rightParams =
        new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
    rightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
    addView(rightButton, rightParams);

    LayoutParams titleParams =
        new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
    titleParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
    addView(titleTextView, titleParams);
  }

做完這一步,這個(gè)自定義標(biāo)題欄其實(shí)就已經(jīng)可以顯示了,來看看吧。

標(biāo)題欄.png

雖然丑了一點(diǎn),不過還算能用。。做實(shí)驗(yàn)就不講究那么多了。到此,就實(shí)現(xiàn)了這個(gè)自定義的標(biāo)題欄,可以自己設(shè)置9個(gè)屬性以及其他系統(tǒng)自帶的屬性,那么僅此就夠了嗎?或許還應(yīng)該給兩邊的按鈕加上點(diǎn)擊事件,但是不能直接寫,直接寫的話就成了固定的事件了,要開放一個(gè)接口給調(diào)用者,讓調(diào)用者自己來編寫邏輯代碼,這樣才能起到復(fù)用的作用。

  /**
   * 定義一個(gè)接口 讓調(diào)用者自己實(shí)現(xiàn)具體邏輯
   */
  public interface MyTopBarClickListener {
    void leftClick();

    void rightClick();
  }

有了接口還要給外部調(diào)用者一個(gè)公開的方法來設(shè)置接口

  /**
   * 開放一個(gè)方法給外部來設(shè)置點(diǎn)擊事件,參數(shù)用接口的形式得到調(diào)用者自己設(shè)置的邏輯
   * @param myTopBarClickListener 監(jiān)聽器接口類
   */
  public void setOnClickListener(MyTopBarClickListener myTopBarClickListener) {
    this.myTopBarClickListener = myTopBarClickListener;
  }

現(xiàn)在外部調(diào)用者可以使用 setOnClickListener 這個(gè)方法來編寫點(diǎn)擊事件的邏輯代碼,我們得到這個(gè)接口的實(shí)現(xiàn)時(shí)候就可以用來填充到確切的點(diǎn)擊事件中了。

 private void setListener() {

    rightButton.setOnClickListener(new OnClickListener() {
      @Override public void onClick(View v) {
        myTopBarClickListener.rightClick();
      }
    });

    leftButton.setOnClickListener(new OnClickListener() {
      @Override public void onClick(View v) {
        myTopBarClickListener.leftClick();
      }
    });
  }

最后,外部調(diào)用者只需要這樣,就可以控制兩個(gè)按鈕的點(diǎn)擊事件了。

    MyTopBar myTopBar = (MyTopBar) findViewById(R.id.mytopbar);
    
    myTopBar.setOnClickListener(new MyTopBar.MyTopBarClickListener() {
      @Override public void leftClick() {
        Toast.makeText(MainActivity.this, "左邊被點(diǎn)擊了", Toast.LENGTH_LONG).show();
      }

      @Override public void rightClick() {
        Toast.makeText(MainActivity.this, "右邊被點(diǎn)擊了", Toast.LENGTH_LONG).show();
      }
    });

上面這段代碼分別為兩個(gè)按鈕設(shè)置了點(diǎn)擊事件,但是具體使用是使用外部調(diào)用者編寫的邏輯。到這里,就實(shí)現(xiàn)由外部調(diào)用者自定義的點(diǎn)擊事件的功能了。那么或許有時(shí)候會(huì)需要更多的功能,比如說有的時(shí)候我只想顯示一個(gè)按鈕,而不是兩個(gè)按鈕都顯示。那么我們可以這樣:

  /**
   * @param i 要控制的按鈕,0為左邊,1為右邊
   * @param x 要顯示還是要隱藏
   */
  public void setButtonVisable(int i, boolean x) {

    if (x) {
      if (i == 0) {
        leftButton.setVisibility(View.VISIBLE);

      }else {
        rightButton.setVisibility(View.VISIBLE);
      }

    }else {
      if (i == 0) {
        leftButton.setVisibility(View.GONE);
      }else {
        rightButton.setTextColor(View.GONE);
      }
    }
  }

在外部調(diào)用下面這行代碼就可以實(shí)現(xiàn)隱藏左邊按鈕的功能了。

myTopBar.setButtonVisable(0,false);
隱藏了左邊按鈕.png

以上,就是組合類型的 View 實(shí)現(xiàn)方式,雖然很丑很簡(jiǎn)單,但是從中可以看出來,通過這種方式可以做出很多復(fù)雜功能的 View 。

重寫


有時(shí)候,不管是繼承原生控件或者是組合原生控件,都不能滿足我們的特殊需求,這種時(shí)候就只能夠自己重頭完全的寫一個(gè)全新的控件了。創(chuàng)建一個(gè)全新的 View 重點(diǎn)在于繪制和交互的部分,通常需要繼承 View 類,并重寫 onDraw() 、onMeasure() 等方法,還可以像剛才的組合控件一樣,引入自定義屬性來豐富控件的可控性。

接下來 我們想實(shí)現(xiàn)一個(gè)效果:中間有一個(gè)實(shí)心圓,外圈是一圈弧線,通過點(diǎn)擊,可以增加弧線的長(zhǎng)度直到360度。我們看看效果圖。

點(diǎn)點(diǎn)圈.png

點(diǎn)擊之后的樣子

點(diǎn)點(diǎn)圈2.png

其中自定義屬性在上面的組合控件中已經(jīng)試用過了,這里為了簡(jiǎn)化代碼就不再使用,來看看繪制和交互的過程吧,我直接貼上所有的代碼,代碼中有詳細(xì)的注釋。

public class MyView extends View {
  Paint circlePaint, arcPaint, textPaint;
  RectF rectF;
  //弧線度數(shù)變量
  int i = 10;

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

  private void initPaint() {
    textPaint = new TextPaint(0);
    textPaint.setColor(Color.RED);
    textPaint.setTextSize(50);
    //實(shí)心圓畫筆
    circlePaint = new Paint(0);
    circlePaint.setColor(Color.BLUE);
    //外圈弧線畫筆
    arcPaint = new Paint(0);
    arcPaint.setColor(Color.GREEN);                    //設(shè)置畫筆顏色
    arcPaint.setStyle(Paint.Style.STROKE);    //設(shè)置不填充中間
    arcPaint.setStrokeWidth((float) 80.0);   //畫筆粗細(xì)

    //用來定位弧線的矩形
    rectF = new RectF();
    rectF.top = 340;
    rectF.bottom = 740;
    rectF.left = 340;
    rectF.right = 740;
  }

  @Override protected void onDraw(Canvas canvas) {

    //這是圓心的坐標(biāo),我的屏幕的1080的,使用540的話就是橫向居中了。
    float xy = 540;
    //圓圈的半徑 這里是100
    float radius = 100;
    //畫圓~
    canvas.drawCircle(xy, xy, radius, circlePaint);

    //畫弧線

    canvas.drawArc(rectF, 270, i, false, arcPaint);

    //顯示度數(shù)與提示
    canvas.drawText(String.valueOf(i), 500, 560, textPaint);
    canvas.drawText("點(diǎn)一點(diǎn)會(huì)轉(zhuǎn)圈!", 400, 860, textPaint);
  }

  /**
   * 公開一個(gè)方法,可以更新弧線的度數(shù)~
   */
  public void ddd() {
    //超過360度就還原到10度
    if (i >= 360) {
      i = 10;
      postInvalidate();
    } else {
      this.i += 10;
      postInvalidate();
    }
  }
}

這就是自定義控件的全部代碼了,在外部只需要簡(jiǎn)單的調(diào)用,就可以控制弧線的度數(shù),以下是外部調(diào)用的例子。

 void initMyView()
  {
    final MyView myView = (MyView) findViewById(R.id.myview);
    myView.setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        myView.ddd();
      }
    });
  }

以上所有代碼可以到開頭的 github 地址下載。

總結(jié)


以上就是三種對(duì) View 進(jìn)行創(chuàng)新的方式。掌握好了這個(gè)才能做出美觀的界面,掌握熟練之后還可以造輪子,做出漂亮的,可控性高的控件供重復(fù)使用。做 Android 大部分時(shí)間都是在做用戶界面的交互,這其中 View 相當(dāng)重要。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,825評(píng)論 25 709
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 7,299評(píng)論 0 17
  • 三月最后一天就要過去,很多朋友都在期待清明假期的到來,但我們不能掉以輕心。不在安逸中為王,就在安逸中滅亡。聰明的人...
    北極雪路閱讀 243評(píng)論 0 0
  • ---國(guó)慶中秋雙節(jié)兩地巡學(xué)記 我是一名基層工作的心理咨詢師,平日里的節(jié)奏非常固定:接受預(yù)約、進(jìn)行個(gè)案、開展公益沙龍...
    深愛閱讀 587評(píng)論 0 2

友情鏈接更多精彩內(nèi)容