文章目录

  • 1. 起由
  • 2. 思路与实现
  • 2.1 创建自定义View
  • 2.2 自定义属性
  • 2.3 看TextView的逻辑
  • 2.4 撸码环节
  • 3. 完整源码


1. 起由

TextView的drawable 无法控制大小,经常会导致效果不如意,不得不采用布局嵌套多个View。效果不好。
因此,扩展TextView功能,使其能自由控制大小。

2. 思路与实现

2.1 创建自定义View

TextView 已经有实现drawableLeft ,drawablePadding 等,因此,我们只要继承并扩展TextView即可。
为了兼容性更好,直接 extends AppCompatTextView 。

2.2 自定义属性

TextView上下左右都可以设置drawable,有可能会有需要单独设置宽高大小的情况。因此,我们需要设置上下左右的宽高,总共8个属性。也有可能需要统一大小,那就再添加一对设置整体 drawable 的宽高设置。因此总共10个属性。如下:

attrs.xml
自定义属性

<resources>
    <!--可以控制 drawable 大小的 TextView-->
    <declare-styleable name="DrawableTextView">
        <attr name="drawableWidth" format="dimension" />
        <attr name="drawableHeight" format="dimension" />

        <attr name="drawableLeftWidth" format="dimension" />
        <attr name="drawableLeftHeight" format="dimension" />

        <attr name="drawableRightWidth" format="dimension" />
        <attr name="drawableRightHeight" format="dimension" />

        <attr name="drawableTopWidth" format="dimension" />
        <attr name="drawableTopHeight" format="dimension" />

        <attr name="drawableBottomWidth" format="dimension" />
        <attr name="drawableBottomHeight" format="dimension" />
    </declare-styleable>
</resources>

2.3 看TextView的逻辑

step 1 . drawable的获取
在构造方法里

public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    // ...省略其它
    Drawable drawableLeft = null, drawableTop = null, drawableRight = null,
                drawableBottom = null, drawableStart = null, drawableEnd = null;
    // ...
    switch (attr) {
      case com.android.internal.R.styleable.TextView_drawableLeft:
           drawableLeft = a.getDrawable(attr);
           break;
      case com.android.internal.R.styleable.TextView_drawableTop:
           drawableTop = a.getDrawable(attr);
           break;
      case com.android.internal.R.styleable.TextView_drawableRight:
           drawableRight = a.getDrawable(attr);
           break;
      case com.android.internal.R.styleable.TextView_drawableBottom:
           drawableBottom = a.getDrawable(attr);
           break;
      // ...
}

step 2. drawable的设置 (重点)
从这里可以看到,setCompoundDrawablesWithIntrinsicBounds 设置了drawable的尺寸。因此我们只需要修改这部分就可以。

@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);
}

2.4 撸码环节

step 1 . 变量的设计与存放

考虑到除了自定义属性外,我们还有可能在java代码中进行调用,每次调用有可能设置某一个drawable的宽高。如果我们都采用单独变量的话,上下左右4个drawable + 4个drawable的宽和高 + 全局设置 ,
那么总共需要:4*2 + 2 + 4 个变量,以及对应的set方法。工作量不可谓不大,而且也不利于查看。
因此,我们可以采用两个数组来表示。一个数组存 drawable,另外一个存其尺寸。代码如下:

public final static int DRAWABLE_POSITION_LEFT = 0;
    public final static int DRAWABLE_POSITION_TOP = 1;
    public final static int DRAWABLE_POSITION_RIGHT = 2;
    public final static int DRAWABLE_POSITION_BOTTOM = 3;

    public final static int SIZE_WIDTH = 0;
    public final static int SIZE_HEIGHT = 1;

    private Drawable[] drawableList;
    private int[][] drawableSizeList = new int[4][2];

step 2 . 在我们的自定义View中获取到drawable

从父类方法中可以看到,TextView是在构造方法中用一个本地变量进行的获取。那么,在自定义View中难以直接使用或以类似方法取值。
当然,也不是没有办法。可以如下获取:

int identifier = Resources.getSystem().getIdentifier("drawableLeft", "attr", "android");
leftDrawable = ta.getDrawable(identifier);

除此之外,我们可以看到 调用 setCompoundDrawablesWithIntrinsicBounds 方法时,会传递过来4个drawable,那么子类可以通过重写此处,来实现drawable的获取。

@Override
    public void setCompoundDrawablesWithIntrinsicBounds(@Nullable Drawable left, @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom) {
        if (drawableList == null) {
            drawableList = new Drawable[4];
        }
        drawableList[DRAWABLE_POSITION_LEFT] = left;
        drawableList[DRAWABLE_POSITION_TOP] = top;
        drawableList[DRAWABLE_POSITION_RIGHT] = right;
        drawableList[DRAWABLE_POSITION_BOTTOM] = bottom;
    }

需要注意的是,这个方法的调用在父类的构造方法,此时子类还没构造完成,因此无法在子类中给drawableList赋值。

step 3. 设置drawable尺寸
我们只需要参考TextView中的 setCompoundDrawablesWithIntrinsicBounds 写法就可以了,照葫芦画瓢。
如下:

public void setDrawableSize() {
        if (drawableList == null) {
            drawableList = new Drawable[4];
        }

        if (drawableList[DRAWABLE_POSITION_LEFT] != null) {
            drawableList[DRAWABLE_POSITION_LEFT].setBounds(0, 0, drawableSizeList[DRAWABLE_POSITION_LEFT][SIZE_WIDTH], drawableSizeList[DRAWABLE_POSITION_LEFT][SIZE_HEIGHT]);
        }
        if (drawableList[DRAWABLE_POSITION_TOP] != null) {
            drawableList[DRAWABLE_POSITION_TOP].setBounds(0, 0, drawableSizeList[DRAWABLE_POSITION_TOP][SIZE_WIDTH], drawableSizeList[DRAWABLE_POSITION_TOP][SIZE_HEIGHT]);
        }
        if (drawableList[DRAWABLE_POSITION_RIGHT] != null) {
            drawableList[DRAWABLE_POSITION_RIGHT].setBounds(0, 0, drawableSizeList[DRAWABLE_POSITION_LEFT][SIZE_WIDTH], drawableSizeList[DRAWABLE_POSITION_RIGHT][SIZE_HEIGHT]);
        }
        if (drawableList[DRAWABLE_POSITION_BOTTOM] != null) {
            drawableList[DRAWABLE_POSITION_BOTTOM].setBounds(0, 0, drawableSizeList[DRAWABLE_POSITION_BOTTOM][SIZE_WIDTH], drawableSizeList[DRAWABLE_POSITION_BOTTOM][SIZE_HEIGHT]);
        }
        setCompoundDrawables(drawableList[DRAWABLE_POSITION_LEFT], drawableList[DRAWABLE_POSITION_TOP],
                drawableList[DRAWABLE_POSITION_RIGHT], drawableList[DRAWABLE_POSITION_BOTTOM]);
    }

step 4. 代码中动态设置
在java代码中动态设置。因为大多数情况每次知识控制一个drawable的尺寸,那么,我们指定其每次传递一个标识position与尺寸,然后进行相关设置即可。
代码如下:

public void setDrawableSize(int position, int width, int height) {
        // 如果数值未发生变化,直接返回
        if (drawableSizeList[position][SIZE_WIDTH] == width && drawableSizeList[position][SIZE_HEIGHT] == height) {
            return;
        }
        drawableSizeList[position][SIZE_WIDTH] = width;
        drawableSizeList[position][SIZE_HEIGHT] = height;

        setDrawableSize();
    }

但是如果,position 传递的并非我们设置的4个值,程序就会报错。那么想到的是可以替换为枚举类型。除此之外,给position加一个注解的限定,是一个更好的解决方案。
这里就要用到java中提供的注解 @IntDef ,自定义注解如下:

@Retention(RetentionPolicy.SOURCE)
    @Target(ElementType.PARAMETER)
    @IntDef({DRAWABLE_POSITION_LEFT, DRAWABLE_POSITION_TOP, DRAWABLE_POSITION_RIGHT, DRAWABLE_POSITION_BOTTOM})
    @interface Position {
    }

然后,用我们的自定义注解去注解参数int position 即可。

3. 完整源码

Github: DrawableTextView.java

另外,自荐一下我整理的View库,https://github.com/zhengweichao/view ,收集了一些常用的轻量级自定义View。如感兴趣,尽可查看。