天天看點

安卓自定義View----實作TextView可設定drawable寬高度前言drawable大小的實作原理自定義TextView----XXDrawableTextView 總結:

安卓自定義View----實作TextView可設定drawable寬高度前言drawable大小的實作原理自定義TextView----XXDrawableTextView 總結:

前言

如上圖所示,相信可愛的安卓程式猿們在開發中經常會遇到這種樣式的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變量。 源碼:

case com.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.
 * <p>
 * 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的屬性,添加下面代碼:

<!--自定義xxDrawableView的屬性-->
<attr name="drawableWidth_left" format="dimension" />
<attr name="drawableHeight_left" format="dimension" />
<attr name="drawableWidth_top" format="dimension" />
<attr name="drawableHeight_top" format="dimension" />
<attr name="drawableWidth_right" format="dimension" />
<attr name="drawableHeight_right" format="dimension" />
<attr name="drawableWidth_bottom" format="dimension" />
<attr name="drawableHeight_bottom" format="dimension" />      

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

然後再添加下面這段:

<declare-styleable name="XXDrawableTextView">
    <attr name="drawableWidth_left" />
    <attr name="drawableHeight_left" />
    <attr name="drawableWidth_top" />
    <attr name="drawableHeight_top" />
    <attr name="drawableWidth_right" />
    <attr name="drawableHeight_right" />
    <attr name="drawableWidth_bottom" />
    <attr name="drawableHeight_bottom" />
</declare-styleable>      

第二步: 

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

/**
 * 獲得我們所定義的自定義樣式屬性
 */
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.XXDrawableTextView, defStyleAttr, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{
    int attr = a.getIndex(i);
    switch (attr)
    {
        case R.styleable.XXDrawableTextView_drawableWidth_left:
            leftDrawableWidth = a.getDimensionPixelSize(attr,10);
            break;
        case R.styleable.XXDrawableTextView_drawableHeight_left:
            leftDrawableHeight = a.getDimensionPixelSize(attr, 10);
            break;

        case R.styleable.XXDrawableTextView_drawableWidth_top:
            topDrawableWidth = a.getDimensionPixelSize(attr,10);
            break;
        case R.styleable.XXDrawableTextView_drawableHeight_top:
            topDrawableHeight = a.getDimensionPixelSize(attr, 10);
            break;

        case R.styleable.XXDrawableTextView_drawableWidth_right:
            rightDrawableWidth = a.getDimensionPixelSize(attr,10);
            break;
        case R.styleable.XXDrawableTextView_drawableHeight_right:
            rightDrawableHeight = a.getDimensionPixelSize(attr, 10);
            break;
        case R.styleable.XXDrawableTextView_drawableWidth_bottom:
            bottomDrawableWidth = a.getDimensionPixelSize(attr,10);
            break;
        case R.styleable.XXDrawableTextView_drawableHeight_bottom:
            bottomDrawableHeight = a.getDimensionPixelSize(attr, 10);
            break;
           }
}
a.recycle();      

第三步:

重寫setCompoundDrawablesWithIntrinsicBounds      

方法,為各個drawable寶寶們設定寬度和高度。

@Override
public void setCompoundDrawablesWithIntrinsicBounds(@Nullable Drawable left,
   @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable 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.XXDrawableTextView
    android: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

package com.xiaxiao.xiaoandroid.customview;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.widget.TextView;

import com.xiaxiao.xiaoandroid.R;

/**
 * Created by xiaxiao on 2017/9/13.
 *
 * 用來解決文字和圖檔組合時造成的view層級過多的問題。
 * 比如上圖示下文字,下圖示上文字,尤其是在實作一組tab均勻平鋪的效果時出現的大量view層級
 * 比如各app的底部欄,本類隻要一層view既可。
 *
 * 注意:必須設定drawable的寬高度。
 *
 */

public class XXDrawableTextView extends TextView {

    public final static int  POSITION_LEFT=0;
    public final static int  POSITION_TOP=1;
    public final static int  POSITION_RIGHT=2;
    public final static int  POSITION_BOTTOM=3;

    int leftDrawableWidth=10;
    int leftDrawableHeight=10;
    int topDrawableWidth=10;
    int topDrawableHeight=10;
    int rightDrawableWidth=10;
    int rightDrawableHeight=10;
    int bottomDrawableWidth=10;
    int bottomDrawableHeight=10;
    Paint mPaint;
    Paint mPaint2;
    Rect mBound;
    Drawable left;
    Drawable top;
    Drawable right;
    Drawable bottom;
    public XXDrawableTextView(Context context) {
        this(context,null,0);
    }

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

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

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public XXDrawableTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        getAttributes(context, attrs, defStyleAttr);
    }


    public void getAttributes(Context context, AttributeSet attrs, int defStyleAttr) {
        /**
         * 獲得我們所定義的自定義樣式屬性
         */
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.XXDrawableTextView, defStyleAttr, 0);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++)
        {
            int attr = a.getIndex(i);
            switch (attr)
            {
                case R.styleable.XXDrawableTextView_drawableWidth_left:
                    leftDrawableWidth = a.getDimensionPixelSize(attr,10);
                    break;
                case R.styleable.XXDrawableTextView_drawableHeight_left:
                    leftDrawableHeight = a.getDimensionPixelSize(attr, 10);
                    break;

                case R.styleable.XXDrawableTextView_drawableWidth_top:
                    topDrawableWidth = a.getDimensionPixelSize(attr,10);
                    break;
                case R.styleable.XXDrawableTextView_drawableHeight_top:
                    topDrawableHeight = a.getDimensionPixelSize(attr, 10);
                    break;

                case R.styleable.XXDrawableTextView_drawableWidth_right:
                    rightDrawableWidth = a.getDimensionPixelSize(attr,10);
                    break;
                case R.styleable.XXDrawableTextView_drawableHeight_right:
                    rightDrawableHeight = a.getDimensionPixelSize(attr, 10);
                    break;
                case R.styleable.XXDrawableTextView_drawableWidth_bottom:
                    bottomDrawableWidth = a.getDimensionPixelSize(attr,10);
                    break;
                case R.styleable.XXDrawableTextView_drawableHeight_bottom:
                    bottomDrawableHeight = a.getDimensionPixelSize(attr, 10);
                    break;
                case R.styleable.XXDrawableTextView_testnumber:
                    System.out.println("啦啦啦啦啦啦啦TextView2_testnumber:"+a.getDimensionPixelSize(attr,10));
                    break;
                case R.styleable.XXDrawableTextView_teststring:
                    System.out.println("啦啦啦啦啦啦啦TextView2_teststring:"+a.getString(attr));
            }
        }
        a.recycle();

        /*
        * setCompoundDrawablesWithIntrinsicBounds方法會首先在父類的構造方法中執行,
        * 彼時執行時drawable的大小還都沒有開始擷取,都是0,
        * 這裡擷取完自定義的寬高屬性後再次調用這個方法,插入drawable的大小
        * */
        setCompoundDrawablesWithIntrinsicBounds(
                left,top,right,bottom);


    }


    /**
     * 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.
     * <p>
     * Calling this method will overwrite any Drawables previously set using
     * {@link #setCompoundDrawablesRelative} or related methods.
     * 這裡重寫這個方法,來設定上下左右的drawable的大小
     *
     * @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
     */
    @Override
    public void setCompoundDrawablesWithIntrinsicBounds(@Nullable Drawable left,
                                                        @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable 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);
    }

    /*
    * 代碼中動态設定drawable的寬高度
    * */
    public void setDrawableSize(int width, int height,int position) {
        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
    protected void onDraw(Canvas canvas) {
        // Draw the background for this view
        super.onDraw(canvas);

       /*
       測試圓角的
       Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas2 = new Canvas(bitmap);

        super.onDraw(canvas2);

        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setAntiAlias(true);
        //16種狀态
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
        mPaint2 = new Paint();
        mPaint2.setColor(Color.YELLOW);
        mPaint2.setXfermode(null);

        int radius=100;
        Path path = new Path();
        path.moveTo(0, radius);
        path.lineTo(0, 0);
        path.lineTo(radius, 0);
        //arcTo的第二個參數是以多少度為開始點,第三個參數-90度表示逆時針畫弧,正數表示順時針
        path.arcTo(new RectF(0, 0, radius * 2, radius * 2), -90, -90);
        path.close();
        canvas2.drawPath(path, mPaint);

        canvas.drawBitmap(bitmap, 0, 0, mPaint2);
        bitmap.recycle();*/

/*
        final int compoundPaddingLeft = getCompoundPaddingLeft();
        final int compoundPaddingTop = getCompoundPaddingTop();
        final int compoundPaddingRight = getCompoundPaddingRight();
        final int compoundPaddingBottom = getCompoundPaddingBottom();
        final int scrollX = getScrollX();
        final int scrollY = getScrollY();
        final int right = getRight();
        final int left = getLeft();
        final int bottom = getBottom();
        final int top = getTop();
        final int offset =0;
        final int leftOffset = 0;
        final int rightOffset =0;


        *//*
        * 0-1-2-3
        * left-top-right-bottom
        * *//*
        Drawable[] drawables = getCompoundDrawables();


            *//*
             * Compound, not extended, because the icon is not clipped
             * if the text height is smaller.
             *//*

            int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
            int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;

            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
            // Make sure to update invalidateDrawable() when changing this code.
            if (drawables[0] != null) {
                canvas.save();
                canvas.translate(scrollX + getPaddingLeft() + leftOffset,
                        scrollY + compoundPaddingTop +
                                (vspace - leftDrawableHeight) / 2);
                drawables[0].draw(canvas);
                canvas.restore();
            }

            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
            // Make sure to update invalidateDrawable() when changing this code.
            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: The coordinates computed are also used in invalidateDrawable()
            // Make sure to update invalidateDrawable() when changing this code.
            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: The coordinates computed are also used in invalidateDrawable()
            // Make sure to update invalidateDrawable() when changing this code.
            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大大們了。

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