android 自定义控件之文字(新闻热门置顶等标签的效果)

之前项目中有个需求,需要在同一行展示n个标签,标签内容有 热门、置顶、广告、促销、精致 等等,都是两个字的词语,并且标签外面需要有个圆角矩形包裹起来,我们可以直接看今日头条的主界面的样式,见下图,途中只有一个标签

我们项目中的情况与这个类似,也是一个列表,列表中的 item 的最下面的一行,显示标签,标签后面是一些时间和商店名等信息。根据条目内容,标签可能为1个、2个、3个或者一个都没有,我接手项目时,这个功能已经实现了。只是列表在上下滑动时,尤其是快速滑动,会有卡滞的感觉,我看了看适配器的代码,发现这个标签的代码写的相当个性:用一个线性布局容器,每次展示item时,先调用容器的 removeAllViews() 方法,删除所有子 view,然后根据业务逻辑,动态创建 TextView,设置文案和背景,然后添加到线性布局中。看到这,感慨怪不得会有微微卡顿的感觉,item 每次展示的时候,都会去创建 view 添加到容器,导致整个 item 都重新绘制,当然耗时耗性能。看到这,这个需要优化。


所谓控件优化,要么是复用对象,提高效率;要么就是自定义,减少层级,一个控件完成n个控件才能完成的效果。


方案一,在适配器中定义个集合,ArrayList<TextView>  mScrapViews = new ArrayList<>(); 既然每次item展现的时候,都会先删除子view,然后再重新创建 view,那为什么不把 remove 的子 view 都放到 mScrapViews 中,创建子 view 的时候,优先去 mScrapViews 中取,如果 mScrapViews 集合元素为空,此时再去 new 一个 view,添加到线性布局中。 在此基础上可再进一步,当item展示时,我们先判断当前item中的标签容器中,有几个子view,标记为 a, 需要展示的标签有几个,标记为b,做个对比,如果 a > b ,说明子 view 多了,需要移除 (a-b) 个子 view,移除的 view 放入 mScrapViews 集合中;如果 a < b ,说明子
view 不够用,需要添加 (b -a ) 个 view,仍然优先去 mScrapViews 集合中取,如果集合中没有元素了,再 new 出来 view,添加到容器中;如果 a = b,则不用多余添加删除 view 的操作。这样操作下来,就相当于避免了大量重复创建 view 的过程,能提高部分效率,但不明显。

方案二,假如说标签最多有6个,那么我们直接在布局中写6个 TextView,然后根据业务逻辑来设置 VISIBLE 或 GONE,这是一种方法,标签少的话可以这么做,如果标签比较多,比如说有15个,这么做的话,findViewById() 就要写一大串代码,不适用,同时对性能有很大影响。


方案三,自定义控件登场。看看标签,我们需要自己绘制圆角方框和文案,圆角方框好说,Canvas 有现成的方法 drawRoundRect() ,可以提供一个矩形 RectF 及圆角的半径大小。绘制文案也有现成的方法 drawText() ,但这里就有一点要说明,文案的y轴的定位,和图形的不太一样,它有基准线的概念,我们看下图


canvas.drawText(String text, float x, float y, Paint paint) 中的第三个参数 y,对应的就是基准线,而不是图中最下面的黑线,那么问题来了,如果我们绘制文案,设置的 y 的值为50,此时的50是基准线 BaseLine,文案的最底部的 bottom 底线,可能是 52,这么就会有细微的偏差,如果对细节要求没那么高可以忽略,但像我们这次说的标签这种情况,就不容忽视了。上图还提供了些信息,基准线和底线的间距叫 Descent,基准线到文案的顶部叫做 Ascent, 这两个值都可以通过 Paint 来获取,descent() 和 ascent() 方法,descent 是正数,ascent 是负数,我们使用时要使用绝对值,如果想在一个 View 的中间绘制文案,计算y轴时,公式为  baseY = (int) ((canvas.getHeight() / 2) + ((Math.abs(mPaint.ascent())-Math.abs(mPaint.descent())) / 2)); 为什么是这样呢? 我们知道,如果 Baseline和屏幕中间线重合,那么文本不会居中,会偏上,因为 ascent 距离大于 descent 的距离。这两个距离,都是以基准线为准的,那么 BaseLine下移多少才能让文案居中呢? 需要 ascent - descent ,然后除以2,这就是偏移量,所以才有了上面的公式。


我们用二进制字符串 "1101" 来表示标签,1 代表有,0代表无,不同位置代表不同的标签。 这样可以随意组合,我们先写个方法来拆分字符串和返回对应的标签文字

class Util{

    final static int[] FLAG_ARR = {1 << 0, 1 << 1, 1 << 2, 1 << 3 };

    public static ArrayList<Integer> takeArr(String s){
        return takeArr(Integer.valueOf(s, 2));
    }

    public static ArrayList<Integer> takeArr(long titleDisplay){
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0, count = FLAG_ARR.length; i < count; i++) {
            long value = FLAG_ARR[i];
            value = value & titleDisplay;
            if (value != 0) {
                list.add(i);
            }
        }
        return list;
    }

    public static String takeDis(int index){
        String s;
        switch (index) {
            case 0:
                s = "广告";
                break;
            case 1:
                s = "置顶";
                break;
            case 2:
                s = "热门";
                break;
            case 3:
                s = "促销";
                break;
            default:
                s = "广告";
                break;
        }
        return s;
    }
}


自定义控件中,我们继承 View, 在 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法中,我们计算下标签的个数,加上间距等值,算出所需要的最小宽度和高度,如果xml布局中设置的是包裹内容,那么我们就用最小宽度和高度来作为控件的宽和高。 在 onDraw(Canvas canvas) 中,我们先画边框,根据边框间距算出每个边框的起始位置,然后可以放到一个数组里,边框画完后,根据数组的值来画出相对应的文案。 见代码

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import java.util.ArrayList;

public class MarkTypeView extends View {

    private static final String MARK_TEXT = "广告";
    private Paint mPaint;
    private int baseX, baseY;

    float[] mLeftArr;
    private int mTextSize = 20;
    int mTopSpace = 5, mLeftSpace = 20, mInnerSpace = 8, mOutInnSpace = 15; // 最后一个是两个边框的间距
    int mRound = mTextSize / 4;
    float mMarkLenght;
    private String mMark = "1001";

    public MarkTypeView(Context context) {
        this(context, null);
    }

    public MarkTypeView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MarkTypeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    private void initPaint() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(Color.RED);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(2);
        mMarkLenght = mPaint.measureText(MARK_TEXT) + mInnerSpace * 2;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int size = Util.takeArr(mMark).size();
        int viewW = (int) (mMarkLenght * size + mLeftSpace * 2 + mOutInnSpace * (size - 1));
        int viewH = (mTextSize + mTopSpace * 2 + mInnerSpace * 2) ;
        setMeasuredDimension(resolveSize(viewW, widthMeasureSpec), resolveSize(viewH, heightMeasureSpec));
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.argb(255,204,204,204));
        mPaint.setStyle(Paint.Style.STROKE);
        ArrayList<Integer> list = Util.takeArr(mMark);

        baseY = (int) ((canvas.getHeight() / 2) - ((mPaint.descent() + mPaint.ascent()) / 2));
        int h = getMeasuredHeight();
        int size = list.size();
        float top = mTopSpace, bottom = h - top ;
        float left,right;
        RectF oval;
        mLeftArr = new float[size];
        for(int i = 0; i < size; i++){
            left = mLeftSpace + i * (mMarkLenght + mOutInnSpace);
            right = left + mMarkLenght;
            oval = new RectF(left, top, right, bottom);
            canvas.drawRoundRect(oval, mRound, mRound, mPaint);
            mLeftArr[i] = left;
        }

        mPaint.setStyle(Paint.Style.FILL);
        for (int i = 0; i < size; i++){
            canvas.drawText(Util.takeDis(i), mLeftArr[i] + mInnerSpace, baseY, mPaint);
        }

    }


    public void setMark(String mark) {
        this.mMark = mark;
        if("0000".equals(mark)){
            setVisibility(View.GONE);
        } else {
            setVisibility(View.VISIBLE);
//            invalidate();
            requestLayout();
        }
    }

}
        <com.view.MarkTypeView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

如果不同的文案想显示不同的颜色,继续扩展即可。上面例子比较简单,像 字体大小、间距 等都可以抽取出来,做成自定义属性,可以在 xml 中配置,或者对外暴露接口,可以在代码中调用。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值