前言
如上图所示,相信可爱的安卓程序猿们在开发中经常会遇到这种样式的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大大们了。
如果帮到你了,给个赞吧。