安卓自定義View----實現TextView可設置drawable寬高度

前言


圖片發自簡書App


如上圖所示,相信可愛的安卓程序猿們在開發中經常會遇到這種樣式的UI開發。其實上面這種布局很簡單,沒有難度,只不過是繁雜的view嵌套而已。通常我們在實現上面這種效果的時候會有3種方式:

方式一:

一層一層的搭建,首先外層是一個橫向的LinearLayout,然后里面包裹著四個LinearLayout作為子View, 每一個Linearlayout里面再寫上一個ImageView和一個TextView.如此簡單的一個布局我們竟然需要父View和子View一共13個View來實現。視圖的層級已經達到了3層。這種方式笨重,低效,耗能。

方式二:

繼承一個布局文件,實現自定義的tabView.這是自定義view中的一種。首先針對上圖中的一個tab寫一個布局文件abc.xml,很簡單,一個LinearLayout裝著一個ImageView和一個TextView,.然后對這個布局文件進行封裝,添加自定義的屬性。這樣在實現上述布局時只要寫一個LinearLayout,里面添加4個TabView就好了。然而,這種方式看起來是簡單了。但實際上和方式一是沒有什么差別的,加載View時,視圖層級依然是3層。 只是看起來簡單了而已。

方式三:

使用TextView的drawableTop屬性。明明有這么方便優雅的實現方式我們卻不用,太是暴殄天物了。于是乎,我們寫一個LinearLayout,里面添上4個TextView,在布局文件中為每一個TextView設置android:drawableTop="@drawable/haha.png"

然后呢,就沒然后了。已經完成了!上述的那個布局樣式就這么輕松加愉快的實現了。視圖層級從原來得分3層變成了現在的兩層,不要小看這一層,在加載xml文件的時候,層級的增加會大大增加對資源和時間的消耗。其次,view個數從原來的13個變成了現在的5個。太棒了。

可是意外就像bug,總在你想不到的地方出現。這么完美的實現方式,到最后我們竟然無法設置TextView加載的drawable的大小!!也就是說資源文件本身寬高多大就只能多大。安卓沒有提供修改這個drawable大小的API.驚不驚喜,意不意外。

那么問題來了。我們到底能不能修改他的大小呢,答案當然是能,這就需要我們通過繼承TextView來改造一下他的方法來實現。接下來我就向大家介紹一下我的思考過程和實現方式,一起看看每一步是否是合理的。

drawable大小的實現原理

首先當然是閱讀源碼了,對此我們需要有一個突破口,這里我就從TextVIew的drawableTop屬性開始。我們在文件中設置了這個屬性,源碼中肯定要有相對應的操作。在TextView的源碼里我們搜索drawableTop,

第一步:

在TextView的構造方法里系統獲取了drawableTop屬性,并復制給drawableTop變量。 源碼:

casecom.android.internal.R.styleable.TextView_drawableTop:drawableTop = a.getDrawable(attr);break;

第二步:

我們查找DrawableTop變量。順著代碼往下一路走來,依然是在構造方法里。當獲取完了上下左右四個drawable后,系統執行了下面這行代碼:

setCompoundDrawablesWithIntrinsicBounds(drawableLeft, drawableTop, drawableRight, drawableBottom);

顯而易見,這個方法對上下左右四個drawable做了處理。

第三步:

進入setCompoundDrawablesWithIntrinsicBounds方法:下面是系統的源碼,代碼不長,主要看四個if判斷, 其實就是為四個drawable分別設置各自的大小。

/** * Sets the Drawables (if any) to appear to the left of, above, to the * right of, and below the text. Use {@code null} if you do not want a * Drawable there. The Drawables' bounds will be set to their intrinsic * bounds. *

* Calling this method will overwrite any Drawables previously set using

* {@link #setCompoundDrawablesRelative} or related methods.

*

* @attr ref android.R.styleable#TextView_drawableLeft

* @attr ref android.R.styleable#TextView_drawableTop

* @attr ref android.R.styleable#TextView_drawableRight

* @attr ref android.R.styleable#TextView_drawableBottom

*/

@android.view.RemotableViewMethod

public void setCompoundDrawablesWithIntrinsicBounds(@Nullable Drawable left,

@Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom) {

if (left != null) {

left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight());

}

if (right != null) {

right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight());

}

if (top != null) {

top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());

}

if (bottom != null) {

bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());

}

setCompoundDrawables(left, top, right, bottom);

}

這個方法很好理解,核心就是setBounds(0,0, top.getIntrinsicWidth(),top.getIntrinsicHeight());

這句話。到這里,就已經很清晰了,系統獲取了我們為TextView設置的drawable,然后就根據drawable自身的大小來設置了要繪制時的邊界大小。所以我們在為TextVIew設置drawable時,圖片是多大,就顯示多大,真是童叟無欺啊。只是苦了我們搬磚的,還得小心翼翼的找UI大大給切圖。

既然問題找到了。那解決就很容易了。我們實現一個自定義TextView,重寫setCompoundDrawablesWithIntrinsicBounds方法,在里面將setBound方法的傳值改為我們設置的大小就OK了。

自定義TextView----XXDrawableTextView

千里之行,始于足下,開始自定義XXDrawableTextView。

第一步:

在style.xml文件中設置XXDrawableTextView的屬性,添加下面代碼:

<attrname="drawableWidth_left"format="dimension"/><attrname="drawableHeight_left"format="dimension"/><attrname="drawableWidth_top"format="dimension"/><attrname="drawableHeight_top"format="dimension"/><attrname="drawableWidth_right"format="dimension"/><attrname="drawableHeight_right"format="dimension"/><attrname="drawableWidth_bottom"format="dimension"/><attrname="drawableHeight_bottom"format="dimension"/>

這里我把上下左右四個為止的drawable都納入處理的范圍了,其實邏輯都一樣。

然后再添加下面這段:

<declare-styleable name="XXDrawableTextView"> <attrname="drawableWidth_left"/>

<attrname="drawableHeight_left"/>

<attrname="drawableWidth_top"/>

<attrname="drawableHeight_top"/>

<attrname="drawableWidth_right"/>

<attrname="drawableHeight_right"/>

<attrname="drawableWidth_bottom"/>

<attrname="drawableHeight_bottom"/>

</declare-styleable>

第二步:

繼承TextView ,獲取自定義的drawable得寬高度屬性值。

*獲得我們所定義的自定義樣式屬性

*/TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.XXDrawableTextView, defStyleAttr,0);intn = a.getIndexCount();for(inti =0; i < n; i++){intattr = a.getIndex(i);switch(attr) {caseR.styleable.XXDrawableTextView_drawableWidth_left:leftDrawableWidth= a.getDimensionPixelSize(attr,10);break;caseR.styleable.XXDrawableTextView_drawableHeight_left:leftDrawableHeight= a.getDimensionPixelSize(attr,10);break;caseR.styleable.XXDrawableTextView_drawableWidth_top:topDrawableWidth= a.getDimensionPixelSize(attr,10);break;caseR.styleable.XXDrawableTextView_drawableHeight_top:topDrawableHeight= a.getDimensionPixelSize(attr,10);break;caseR.styleable.XXDrawableTextView_drawableWidth_right:rightDrawableWidth= a.getDimensionPixelSize(attr,10);break;caseR.styleable.XXDrawableTextView_drawableHeight_right:rightDrawableHeight= a.getDimensionPixelSize(attr,10);break;caseR.styleable.XXDrawableTextView_drawableWidth_bottom:bottomDrawableWidth= a.getDimensionPixelSize(attr,10);break;caseR.styleable.XXDrawableTextView_drawableHeight_bottom:bottomDrawableHeight= a.getDimensionPixelSize(attr,10);break; }}a.recycle();

第三步:

重寫setCompoundDrawablesWithIntrinsicBounds方法,為各個drawable寶寶們設置寬度和高度。


@Overridepublic voidsetCompoundDrawablesWithIntrinsicBounds(@NullableDrawable left,@NullableDrawable top,@NullableDrawable right,@NullableDrawable bottom) {this.left= left;this.top= top;this.right= right;this.bottom= bottom; System.out.println("啦啦啦啦啦啦啦");if(left !=null) { left.setBounds(0,0,leftDrawableWidth,leftDrawableHeight); }if(right !=null) { right.setBounds(0,0,rightDrawableWidth,rightDrawableHeight); }if(top !=null) { top.setBounds(0,0,topDrawableWidth,topDrawableHeight); }if(bottom !=null) { bottom.setBounds(0,0,bottomDrawableWidth,bottomDrawableHeight); } setCompoundDrawables(left, top, right, bottom);}

你看 ,其實最關鍵的還是setBound方法,將我們獲取到的寬高度傳了進去。

第四步:

到這,自定義View的基本工作已經做完了,我們可以在布局文件中使用了,

注意 ,因為是自定義view,一定不要忘記在布局文件頭部添加

xmlns:app="http://schemas.android.com/apk/res-auto"哦。

最后寫一個LinearLayout,里面就替換成四個我們自定義的XXDrawableTextView,輕輕的為每一個XXDrawableTextView設置drawableHeight和drawableWidth屬性。就像下面這樣:

<com.xiaxiao.xiaoandroid.customview.XXDrawableTextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="天氣不錯"android:drawableTop="@drawable/tab2"app:drawableHeight_top="40dp"app:drawableWidth_top="40dp"android:gravity="center_horizontal"android:layout_weight="1"/>

靜悄悄的,簡潔的就像什么都沒發生一樣,然而一切卻變了,我們優雅的實現了tab導航欄的效果,層級依然是2,view個數依然是最少的5個。App的運行效率和性能就這么不經意的被我們提高了那么一丟丟。

下面是具體的自定義XXDrawableTextView類:

XXDrawableTextView.java

[java]view plaincopy

packagecom.xiaxiao.xiaoandroid.customview;

importandroid.annotation.TargetApi;

importandroid.content.Context;

importandroid.content.res.TypedArray;

importandroid.graphics.Canvas;

importandroid.graphics.Paint;

importandroid.graphics.Rect;

importandroid.graphics.drawable.Drawable;

importandroid.os.Build;

importandroid.support.annotation.Nullable;

importandroid.util.AttributeSet;

importandroid.widget.TextView;

importcom.xiaxiao.xiaoandroid.R;

/**

*Createdbyxiaxiaoon2017/9/13.

*

*用來解決文字和圖片組合時造成的view層級過多的問題。

*比如上圖標下文字,下圖標上文字,尤其是在實現一組tab均勻平鋪的效果時出現的大量view層級

*比如各app的底部欄,本類只要一層view既可。

*

*注意:必須設置drawable的寬高度。

*

*/

publicclassXXDrawableTextViewextendsTextView{

publicfinalstaticintPOSITION_LEFT=0;

publicfinalstaticintPOSITION_TOP=1;

publicfinalstaticintPOSITION_RIGHT=2;

publicfinalstaticintPOSITION_BOTTOM=3;

intleftDrawableWidth=10;

intleftDrawableHeight=10;

inttopDrawableWidth=10;

inttopDrawableHeight=10;

intrightDrawableWidth=10;

intrightDrawableHeight=10;

intbottomDrawableWidth=10;

intbottomDrawableHeight=10;

PaintmPaint;

PaintmPaint2;

RectmBound;

Drawableleft;

Drawabletop;

Drawableright;

Drawablebottom;

publicXXDrawableTextView(Contextcontext){

this(context,null,0);

}

publicXXDrawableTextView(Contextcontext,AttributeSetattrs){

this(context,attrs,0);

}

publicXXDrawableTextView(Contextcontext,AttributeSetattrs,intdefStyleAttr){

super(context,attrs,defStyleAttr);

getAttributes(context,attrs,defStyleAttr);

}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)

publicXXDrawableTextView(Contextcontext,AttributeSetattrs,intdefStyleAttr,intdefStyleRes){

super(context,attrs,defStyleAttr,defStyleRes);

getAttributes(context,attrs,defStyleAttr);

}

publicvoidgetAttributes(Contextcontext,AttributeSetattrs,intdefStyleAttr){

/**

*獲得我們所定義的自定義樣式屬性

*/

TypedArraya=context.getTheme().obtainStyledAttributes(attrs,R.styleable.XXDrawableTextView,defStyleAttr,0);

intn=a.getIndexCount();

for(inti=0;i<n;i++)

{

intattr=a.getIndex(i);

switch(attr)

{

caseR.styleable.XXDrawableTextView_drawableWidth_left:

leftDrawableWidth=a.getDimensionPixelSize(attr,10);

break;

caseR.styleable.XXDrawableTextView_drawableHeight_left:

leftDrawableHeight=a.getDimensionPixelSize(attr,10);

break;

caseR.styleable.XXDrawableTextView_drawableWidth_top:

topDrawableWidth=a.getDimensionPixelSize(attr,10);

break;

caseR.styleable.XXDrawableTextView_drawableHeight_top:

topDrawableHeight=a.getDimensionPixelSize(attr,10);

break;

caseR.styleable.XXDrawableTextView_drawableWidth_right:

rightDrawableWidth=a.getDimensionPixelSize(attr,10);

break;

caseR.styleable.XXDrawableTextView_drawableHeight_right:

rightDrawableHeight=a.getDimensionPixelSize(attr,10);

break;

caseR.styleable.XXDrawableTextView_drawableWidth_bottom:

bottomDrawableWidth=a.getDimensionPixelSize(attr,10);

break;

caseR.styleable.XXDrawableTextView_drawableHeight_bottom:

bottomDrawableHeight=a.getDimensionPixelSize(attr,10);

break;

caseR.styleable.XXDrawableTextView_testnumber:

System.out.println("啦啦啦啦啦啦啦TextView2_testnumber:"+a.getDimensionPixelSize(attr,10));

break;

caseR.styleable.XXDrawableTextView_teststring:

System.out.println("啦啦啦啦啦啦啦TextView2_teststring:"+a.getString(attr));

}

}

a.recycle();

/*

*setCompoundDrawablesWithIntrinsicBounds方法會首先在父類的構造方法中執行,

*彼時執行時drawable的大小還都沒有開始獲取,都是0,

*這里獲取完自定義的寬高屬性后再次調用這個方法,插入drawable的大小

**/

setCompoundDrawablesWithIntrinsicBounds(

left,top,right,bottom);

}

/**

*SetstheDrawables(ifany)toappeartotheleftof,above,tothe

*rightof,andbelowthetext.Use{@codenull}ifyoudonotwanta

*Drawablethere.TheDrawables'boundswillbesettotheirintrinsic

*bounds.

*

*CallingthismethodwilloverwriteanyDrawablespreviouslysetusing

*{@link#setCompoundDrawablesRelative}orrelatedmethods.

*這里重寫這個方法,來設置上下左右的drawable的大小

*

*@attrrefandroid.R.styleable#TextView_drawableLeft

*@attrrefandroid.R.styleable#TextView_drawableTop

*@attrrefandroid.R.styleable#TextView_drawableRight

*@attrrefandroid.R.styleable#TextView_drawableBottom

*/

@Override

publicvoidsetCompoundDrawablesWithIntrinsicBounds(@NullableDrawableleft,

@NullableDrawabletop,@NullableDrawableright,@NullableDrawablebottom){

this.left=left;

this.top=top;

this.right=right;

this.bottom=bottom;

System.out.println("啦啦啦啦啦啦啦");

if(left!=null){

left.setBounds(0,0,leftDrawableWidth,leftDrawableHeight);

}

if(right!=null){

right.setBounds(0,0,rightDrawableWidth,rightDrawableHeight);

}

if(top!=null){

top.setBounds(0,0,topDrawableWidth,topDrawableHeight);

}

if(bottom!=null){

bottom.setBounds(0,0,bottomDrawableWidth,bottomDrawableHeight);

}

setCompoundDrawables(left,top,right,bottom);

}

/*

*代碼中動態設置drawable的寬高度

**/

publicvoidsetDrawableSize(intwidth,intheight,intposition){

if(position==this.POSITION_LEFT){

leftDrawableWidth=width;

leftDrawableHeight=height;

}

if(position==this.POSITION_TOP){

topDrawableWidth=width;

topDrawableHeight=height;

}

if(position==this.POSITION_RIGHT){

rightDrawableWidth=width;

rightDrawableHeight=height;

}

if(position==this.POSITION_BOTTOM){

bottomDrawableWidth=width;

bottomDrawableHeight=height;

}

setCompoundDrawablesWithIntrinsicBounds(

left,top,right,bottom);

}

@Override

protectedvoidonDraw(Canvascanvas){

//Drawthebackgroundforthisview

super.onDraw(canvas);

/*

測試圓角的

Bitmapbitmap=Bitmap.createBitmap(getWidth(),getHeight(),Bitmap.Config.ARGB_8888);

Canvascanvas2=newCanvas(bitmap);

super.onDraw(canvas2);

mPaint=newPaint();

mPaint.setColor(Color.RED);

mPaint.setAntiAlias(true);

//16種狀態

mPaint.setXfermode(newPorterDuffXfermode(PorterDuff.Mode.DST_OUT));

mPaint2=newPaint();

mPaint2.setColor(Color.YELLOW);

mPaint2.setXfermode(null);

intradius=100;

Pathpath=newPath();

path.moveTo(0,radius);

path.lineTo(0,0);

path.lineTo(radius,0);

//arcTo的第二個參數是以多少度為開始點,第三個參數-90度表示逆時針畫弧,正數表示順時針

path.arcTo(newRectF(0,0,radius*2,radius*2),-90,-90);

path.close();

canvas2.drawPath(path,mPaint);

canvas.drawBitmap(bitmap,0,0,mPaint2);

bitmap.recycle();*/

/*

finalintcompoundPaddingLeft=getCompoundPaddingLeft();

finalintcompoundPaddingTop=getCompoundPaddingTop();

finalintcompoundPaddingRight=getCompoundPaddingRight();

finalintcompoundPaddingBottom=getCompoundPaddingBottom();

finalintscrollX=getScrollX();

finalintscrollY=getScrollY();

finalintright=getRight();

finalintleft=getLeft();

finalintbottom=getBottom();

finalinttop=getTop();

finalintoffset=0;

finalintleftOffset=0;

finalintrightOffset=0;

*//*

*0-1-2-3

*left-top-right-bottom

**//*

Drawable[]drawables=getCompoundDrawables();

*//*

*Compound,notextended,becausetheiconisnotclipped

*ifthetextheightissmaller.

*//*

intvspace=bottom-top-compoundPaddingBottom-compoundPaddingTop;

inthspace=right-left-compoundPaddingRight-compoundPaddingLeft;

//IMPORTANT:ThecoordinatescomputedarealsousedininvalidateDrawable()

//MakesuretoupdateinvalidateDrawable()whenchangingthiscode.

if(drawables[0]!=null){

canvas.save();

canvas.translate(scrollX+getPaddingLeft()+leftOffset,

scrollY+compoundPaddingTop+

(vspace-leftDrawableHeight)/2);

drawables[0].draw(canvas);

canvas.restore();

}

//IMPORTANT:ThecoordinatescomputedarealsousedininvalidateDrawable()

//MakesuretoupdateinvalidateDrawable()whenchangingthiscode.

if(dr.mShowing[Drawables.RIGHT]!=null){

canvas.save();

canvas.translate(scrollX+right-left-mPaddingRight

-dr.mDrawableSizeRight-rightOffset,

scrollY+compoundPaddingTop+(vspace-dr.mDrawableHeightRight)/2);

dr.mShowing[Drawables.RIGHT].draw(canvas);

canvas.restore();

}

//IMPORTANT:ThecoordinatescomputedarealsousedininvalidateDrawable()

//MakesuretoupdateinvalidateDrawable()whenchangingthiscode.

if(dr.mShowing[Drawables.TOP]!=null){

canvas.save();

canvas.translate(scrollX+compoundPaddingLeft+

(hspace-dr.mDrawableWidthTop)/2,scrollY+mPaddingTop);

dr.mShowing[Drawables.TOP].draw(canvas);

canvas.restore();

}

//IMPORTANT:ThecoordinatescomputedarealsousedininvalidateDrawable()

//MakesuretoupdateinvalidateDrawable()whenchangingthiscode.

if(dr.mShowing[Drawables.BOTTOM]!=null){

canvas.save();

canvas.translate(scrollX+compoundPaddingLeft+

(hspace-dr.mDrawableWidthBottom)/2,

scrollY+bottom-top-mPaddingBottom-dr.mDrawableSizeBottom);

dr.mShowing[Drawables.BOTTOM].draw(canvas);

canvas.restore();

}

canvas.restore();*/

}

}

其中注釋掉的是設置drawable為圓角的嘗試,可忽略。

我還添加了個修改寬高度的方法,可以運行時在代碼中設置drawable的寬高。

其次還需要注意一下setCompoundDrawablesWithIntrinsicBounds方法的調用位置。

因為這個方法是在父類的構造方法中調用的,也就是說當執行XXDrawableTextView的構造方法時,

首先會執行父類的構造方法,在執行super方法時,這個方法已經進行了。這時候getAttribute方法還沒調用呢,

也就是說各個寬高度屬性值都還沒獲得,所以需要在執行完getArttribute方法后再調用一遍

setCompoundDrawablesWithIntrinsicBounds。

總結:

優點呢,簡潔明了,就那么回事,缺點呢就是不能針對其中的drawable再做進一步的處理了,比如設置成圓角之類。嘗試了一下自定義,發現太麻煩了。如果真的出現圖片設置成圓角的場景,

恐怕還得使用TextView加自定義的圓角ImageView。或者,找UI大大們了。

如果幫到你了,給個贊吧。

另:
簡書的編輯環境咋就這么差勁呢,代碼連個換行都不行,瞧這排版,醉了。

觀看友好版請移步:http://blog.csdn.net/xx23x/article/details/77997565

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容