Android文本排版实现

来源:互联网 发布:矩阵a与b相似,a e与b e 编辑:程序博客网 时间:2024/05/16 16:16

        在项目中有一个小功能需要实现,就是对多行文本进行排版布局,每一行的内容又分为两部分,左边为标题,右边为描述,左边内容长度不确定,右边的内容需要对齐,如有换行也需要对齐右边的文本。

效果图

       效果图如下图所示:

这里写图片描述

       可以看到内容分成了两部分,左边的颜色与右边不一致,右边的描述文案统一对齐。

实现方案

       以上功能,由于输入内容输入行数不确定,并且左边的文案长度也不确定,因此不能直接在布局中实现,基于此这里主要实现了以下6种方式

方案1

       采用自定义控件的方式,继承TextView,重新onDraw函数,实现如下:

/** * 计算出左边最长的显示字符串maxLeftWidth,之后draw每一行字符,右边的描述从maxLeftWidth开始draw * 当一行显示不完全时,折行并且空出maxLeftWidth的空格长度 */public class TypographyView1 extends TextView {    private Paint leftPaint = new Paint();    private Paint rightPaint = new Paint();    private int fullWidth;    private float textSize;    private JSONArray array;    private int middlePadding = 0;    float maxLeftWidth = 0;    int itemSize = 0;    public TypographyView1(Context context) {        super(context);        init();    }    public TypographyView1(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    public TypographyView1(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init() {        textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13);        leftPaint.setAntiAlias(true);        leftPaint.setTextSize(textSize);        leftPaint.setColor(getResources().getColor(R.color.color_black_999999));        rightPaint.setAntiAlias(true);        rightPaint.setTextSize(textSize);        rightPaint.setColor(getResources().getColor(R.color.color_black));        middlePadding = getResources().getDimensionPixelSize(R.dimen.padding_value);    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        fullWidth = getWidth();// 整个textView的宽度    }    public void setText(JSONArray array) {        this.array = array;        if (array != null) {            try {                int size = itemSize = array.length();                for (int i = 0; i < size; ++i) {                    JSONArray o = (JSONArray) array.get(i);                    String key = o.getString(0);                    String value = o.getString(1);                    if (TextUtils.isEmpty(key) || TextUtils.isEmpty(value)) {                        itemSize--;                        continue;                    }                    float curWidth = leftPaint.measureText(key);                    if (curWidth > maxLeftWidth) {                        maxLeftWidth = curWidth;                    }                }                maxLeftWidth = maxLeftWidth + middlePadding;                invalidate();            } catch (Exception e) {            }        }    }    boolean setHeight = false;    @Override    protected void onDraw(Canvas canvas) {        if (array == null) {            return;        }        int lineCount = 0;        try {            JSONArray item;            float offsetY;            for (int i = 0; i < itemSize; ++i) {                item = (JSONArray) array.get(i);                offsetY = (lineCount + 1) * textSize;                canvas.drawText(item.getString(0), 0, offsetY, leftPaint);                String value = item.getString(1);                float valueWidth = rightPaint.measureText(value);                if (valueWidth > fullWidth - maxLeftWidth) {// 一行显示不完                    char[] textCharArray = value.toCharArray();                    float charWidth;                    float drawWidth = maxLeftWidth;                    for (int j = 0; j < textCharArray.length; j++) {                        charWidth = rightPaint.measureText(textCharArray, j, 1);                        if (fullWidth - drawWidth < charWidth) {                            lineCount++;                            drawWidth = maxLeftWidth;                            offsetY += textSize;                        }                        canvas.drawText(textCharArray, j, 1, drawWidth, offsetY, rightPaint);                        drawWidth += charWidth;                    }                } else {                    canvas.drawText(value, maxLeftWidth, offsetY, rightPaint);                }                lineCount += 2;            }            if (!setHeight) {                setHeight((lineCount + 1) * (int) textSize);                setHeight = true;            }        } catch (JSONException e) {            e.printStackTrace();        }    }}

       添加了setText(JSONArray array)作为数据输入,并且在这里面测量了左边title的最大宽度,之后调用invalidate触发重绘,在onSizeChanged获取整个控件的宽度,重绘会调用onDraw函数,这里不需要调用super函数,TextView的onDraw函数做了非常多的操作,解析传入的数据,分别一行一行调用canvas来进行drawText操作,当绘制描述时,先计算宽度,如果超过剩余控件说明需要换行,最后调用setHeight设置高度,这个加一个判断条件,因为会触发requestLayout()进行重新布局和invalidate()进行重绘,如果不加判断会一直重绘。

方案2

       方式2与方式1差不多,不同为所有计算都在onDraw函数中:

/** * 该方式与方式1很类似,只是所有的计算都放在了onDraw方法中。 */public class TypographyView2 extends TextView {    private Paint paint1 = new Paint();    private Paint paint2 = new Paint();    private int middlePadding = 0;    int width;    private float textSize;    private JSONArray array;    public TypographyView2(Context context) {        super(context);        init();    }    public TypographyView2(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    public TypographyView2(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init() {        textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13);        paint1.setAntiAlias(true);        paint1.setTextSize(textSize);        paint1.setColor(getResources().getColor(R.color.color_black_999999));        paint2.setAntiAlias(true);        paint2.setTextSize(textSize);        paint2.setColor(getResources().getColor(R.color.color_black));        middlePadding = getResources().getDimensionPixelSize(R.dimen.padding_value);    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        width = getWidth();// 整个textView的宽度    }    public void setText(JSONArray array) {        this.array = array;        if (array != null) {            invalidate();        }    }    boolean setHeight = false;    @Override    protected void onDraw(Canvas canvas) {        // super.onDraw(canvas);        int lineCount = 0;        int size = array.length();        float maxLeftWidth = 0;        float drawWidth = 0;        try {            for (int i = 0; i < size; ++i) {                JSONArray o = (JSONArray) array.get(i);                String key = o.getString(0);                float v = paint1.measureText(key);                if (v > maxLeftWidth) {                    maxLeftWidth = v;                }            }            maxLeftWidth = maxLeftWidth + middlePadding;            for (int i = 0; i < size; ++i) {                JSONArray o = (JSONArray) array.get(i);                String key = o.getString(0);                canvas.drawText(key, 0, (lineCount + 1) * textSize, paint1);                String value = o.getString(1);                char[] textCharArray = value.toCharArray();                float charWidth;                drawWidth = maxLeftWidth;                for (int j = 0; j < textCharArray.length; j++) {                    charWidth = paint1.measureText(textCharArray, j, 1);                    if (width - drawWidth < charWidth) {                        lineCount++;                        drawWidth = maxLeftWidth;                    }                    canvas.drawText(textCharArray, j, 1, drawWidth, (lineCount + 1) * textSize, paint2);                    drawWidth += charWidth;                }                lineCount += 2;            }            if (!setHeight) {                setHeight((lineCount + 1) * (int) textSize + 5);                setHeight = true;            }        } catch (JSONException e) {            e.printStackTrace();        }    }}

       该方案的实现是不太好的,方案1也是在此基础上进行调整的,在这里放出来只是为了说明,所有的计算不要全部放在onDraw里面,因为该方法可能会反复调用多次,这样就降低了性能。

方案3

       将数据源拼接成SpannableString,重写onDraw函数,根据内容draw每一个字符:

/** * 该方法,是需要显示的内容先拼接成SpannableString,在onDraw方法中获取所有的char字符,一个一个比较 * 当为分号是,表示为key与value的分隔符。 */public class TypographyView3 extends TextView {    private Paint leftPaint = new Paint();    private Paint rightPaint = new Paint();    int width;    private String text;    private float textSize;    float maxLeftWidth = 0;    private int middlePadding = 0;    public TypographyView3(Context context) {        super(context);        init();    }    public TypographyView3(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    public TypographyView3(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init() {        textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13);        textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13);        leftPaint.setAntiAlias(true);        leftPaint.setTextSize(textSize);        leftPaint.setColor(getResources().getColor(R.color.color_black_999999));        rightPaint.setAntiAlias(true);        rightPaint.setTextSize(textSize);        rightPaint.setColor(getResources().getColor(R.color.color_black));        middlePadding = getResources().getDimensionPixelSize(R.dimen.padding_value);    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        width = getWidth();// 整个textView的宽度    }    public void setText(JSONArray data) {        if (data == null) {            return;        }        try {            int size = data.length();            for (int i = 0; i < size; ++i) {                JSONArray o = (JSONArray) data.get(i);                String key = o.getString(0);                float v = leftPaint.measureText(key);                if (v > maxLeftWidth) {                    maxLeftWidth = v;                }            }            maxLeftWidth += middlePadding;            SpannableStringBuilder ssb = new SpannableStringBuilder();            for (int i = 0; i < size; ++i) {                addItem((JSONArray) data.get(i), ssb, i != 0);            }            setText(ssb, BufferType.SPANNABLE);        } catch (Exception e) {        }    }    private void addItem(JSONArray item, SpannableStringBuilder ssb, boolean breakLine) {        try {            if (item == null || item.length() == 0) {                return;            }            String key = item.getString(0);            String value = (item.length() >= 2) ? item.getString(1) : "";            if (TextUtils.isEmpty(key) && TextUtils.isEmpty(value)) {                return;            }            if (breakLine) {// 换行                ssb.append("\r\n");                ssb.append("\r\n");            }            SpannableString span = new SpannableString(key);            //            span.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorAccent)), 0, key            // .length(),            //                    Spanned.SPAN_INCLUSIVE_EXCLUSIVE);            ssb.append(span);            ssb.append(value);        } catch (JSONException e) {            e.printStackTrace();        }    }    @Override    protected void onDraw(Canvas canvas) {        // super.onDraw(canvas);        int lineCount = 0;        text = this.getText().toString();        if (text == null)            return;        char[] textCharArray = text.toCharArray();        // 已绘的宽度        float drawWidth = 0;        float charWidth;        Paint paint = leftPaint;        for (int i = 0; i < textCharArray.length; i++) {            charWidth = leftPaint.measureText(textCharArray, i, 1);            if (textCharArray[i] == '\n') {                lineCount++;                drawWidth = 0;                paint = leftPaint;                continue;            }            if (width - drawWidth < charWidth) {                lineCount++;                drawWidth = maxLeftWidth;            }            if (i > 1 && textCharArray[i - 1] == ':') {                drawWidth = maxLeftWidth;                paint = rightPaint;            }            canvas.drawText(textCharArray, i, 1, drawWidth, (lineCount + 1) * textSize, paint);            drawWidth += charWidth;        }        //may be need set height        //setHeight((lineCount + 1) * (int) textSize + 5);    }}

       这里先计算左边title的最大宽度,同时将所有的数据拼接成一个SpannableStringBuilder,调用setText函数会触发重绘,在onDraw函数中进行处理,由于未重新super函数,因此SpannableString的setSpan函数失效,该方案主要根据分隔符来进行分割,因此分隔符需要唯一。

方案4

       采用GridLayout方式实现,但是原始控件有展示问题,因此对此进行了修改:

public class Typography4Activity extends BaseActivity {    public static void start(Context context) {        Intent intent = new Intent();        intent.setClass(context, Typography4Activity.class);        context.startActivity(intent);    }    private LinearLayout root;    private Paint leftPaint = new Paint();    private float textSize;    private float maxLeftWidth;    private int middlePadding = 0;    private float maxRightWidth;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        root = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.activity_typography4, null);        setContentView(root);        initPaint();        findViews();        loadData();    }    private void initPaint() {        textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13);        leftPaint.setAntiAlias(true);        leftPaint.setTextSize(textSize);        leftPaint.setColor(getResources().getColor(R.color.color_black_999999));        middlePadding = getResources().getDimensionPixelSize(R.dimen.padding_value);    }    private void findViews() {    }    private void loadData() {        addGridLayout(DataSource.getArray());        TextView view = new TextView(this);        view.setText("修改后的实现");        view.setGravity(Gravity.CENTER);        view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 160));        root.addView(view);        addModifyGridLayout(DataSource.getArray());    }    private void addGridLayout(JSONArray data) {        try {            GridLayout layout = createGridLayout();            int size = data.length();            for (int i = 0; i < size; ++i) {                JSONArray item = (JSONArray) data.get(i);                String key = item.getString(0);                String value = (item.length() >= 2) ? item.getString(1) : "";                GridLayout.Spec row = GridLayout.spec(i);                GridLayout.Spec col1 = GridLayout.spec(0);                GridLayout.Spec col2 = GridLayout.spec(1);                GridLayout.LayoutParams params = new GridLayout.LayoutParams(row, col1);                TextView title = getLeftTextView(key);                layout.addView(title, params);                params = new GridLayout.LayoutParams(row, col2);                TextView desc = getRightTextView(value);                layout.addView(desc, params);            }            root.addView(layout);        } catch (Exception e) {        }    }    @NonNull    private TextView getRightTextView(String value) {        TextView desc = new TextView(this);        desc.setTextSize(13);        desc.setTextColor(getResources().getColor(R.color.black));        desc.setText(value);        return desc;    }    @NonNull    private TextView getLeftTextView(String key) {        TextView title = new TextView(this);        title.setText(key);        title.setPadding(0, middlePadding, middlePadding, 0);        title.setTextColor(getResources().getColor(R.color.color_black_999999));        title.setTextSize(13);        return title;    }    private void addModifyGridLayout(JSONArray data) {        try {            calculateLeftMaxWidth(data);            GridLayout layout = createGridLayout();            int size = data.length();            for (int i = 0; i < size; ++i) {                JSONArray item = (JSONArray) data.get(i);                GridLayout.Spec row = GridLayout.spec(i);                String key = item.getString(0);                GridLayout.Spec col1 = GridLayout.spec(0);                GridLayout.LayoutParams params = new GridLayout.LayoutParams(row, col1);                TextView title = getLeftTextView(key);                layout.addView(title, params);                String value = (item.length() >= 2) ? item.getString(1) : "";                GridLayout.Spec col2 = GridLayout.spec(1);                params = new GridLayout.LayoutParams(row, col2);                TextView desc = getRightTextView(value);                params.width = (int) maxRightWidth;                params.height = ViewGroup.LayoutParams.WRAP_CONTENT;                layout.addView(desc, params);            }            root.addView(layout);        } catch (Exception e) {        }    }    private void calculateLeftMaxWidth(JSONArray data) {        try {            DisplayUtil.init(this);// 这个可以在应用程序起来的时候init            int size = data.length();            for (int i = 0; i < size; ++i) {                JSONArray o = (JSONArray) data.get(i);                String key = o.getString(0);                String value = o.getString(1);                if (TextUtils.isEmpty(key) || TextUtils.isEmpty(value)) {                    continue;                }                float curWidth = leftPaint.measureText(key);                if (curWidth > maxLeftWidth) {                    maxLeftWidth = curWidth;                }            }            maxLeftWidth = maxLeftWidth + middlePadding;            maxRightWidth = DisplayUtil.screenWidth - DisplayUtil.dp2px(this, 32 + 10) - maxLeftWidth;        } catch (Exception e) {        }    }    private GridLayout createGridLayout() {        GridLayout layout = new GridLayout(this);        layout.setColumnCount(2);        //layout.setRowCount(5);        layout.setOrientation(GridLayout.HORIZONTAL);        return layout;    }}

       如果直接创建一个GridLayout,里面添加每一项,如果描述过长都导致显示不全,这个是系统的一个bug,计算的宽度有问题,因此需要对此方案进行更改。
       更改方式为先计算左边占用的最大宽度,在添加右边的项时,设置布局参数控制最大的长度。

方案5

       采用每一行一个布局,手动一行一行进行添加:

public class Typography5Activity extends BaseActivity {    public static void start(Context context) {        Intent intent = new Intent();        intent.setClass(context, Typography5Activity.class);        context.startActivity(intent);    }    private LinearLayout root;    private Paint leftPaint = new Paint();    private float textSize;    private float maxLeftWidth;    private int middlePadding = 0;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        root = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.activity_typography5, null);        setContentView(root);        initPaint();        loadData();    }    private void initPaint() {        textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13);        leftPaint.setAntiAlias(true);        leftPaint.setTextSize(textSize);        leftPaint.setColor(getResources().getColor(R.color.color_black_999999));        middlePadding = getResources().getDimensionPixelSize(R.dimen.padding_value);    }    private void loadData() {        JSONArray array = DataSource.getArray();        calculateLeftMaxWidth(array);        if (array != null) {            try {                int size = array.length();                for (int i = 0; i < size; ++i) {                    JSONArray o = (JSONArray) array.get(i);                    String key = o.getString(0);                    String value = o.getString(1);                    addItem(key, value);                }            } catch (Exception e) {            }        }    }    private void calculateLeftMaxWidth(JSONArray data) {        try {            int size = data.length();            for (int i = 0; i < size; ++i) {                JSONArray o = (JSONArray) data.get(i);                String key = o.getString(0);                String value = o.getString(1);                if (TextUtils.isEmpty(key) || TextUtils.isEmpty(value)) {                    continue;                }                float curWidth = leftPaint.measureText(key);                if (curWidth > maxLeftWidth) {                    maxLeftWidth = curWidth;                }            }            maxLeftWidth = maxLeftWidth + middlePadding;        } catch (Exception e) {        }    }    private void addItem(String key, String value) {        LinearLayout layout = getItemLayout();        TextView left = (TextView) layout.findViewById(R.id.left);        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,                ViewGroup.LayoutParams.WRAP_CONTENT);        params.width = (int) maxLeftWidth;        left.setLayoutParams(params);        left.setText(key);        TextView right = (TextView) layout.findViewById(R.id.right);        right.setText(value);        root.addView(layout);    }    private LinearLayout getItemLayout() {        LinearLayout layout = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.compose_item_layout, null);        return layout;    }}

       改方案也需要先计算左边的最大占用宽度,来设置右边占用的大小,每一项的布局如下:

<?xml version="1.0" encoding="utf-8"?><LinearLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="horizontal"    android:paddingTop="@dimen/text_padding_10"    tools:context=".activity.Typography1Activity">    <TextView        android:id="@+id/left"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_marginRight="@dimen/text_padding_10"        android:textColor="@color/color_black_999999"        android:textSize="@dimen/text_size_13"/>    <TextView        android:id="@+id/right"        android:layout_width="0dp"        android:layout_height="wrap_content"        android:layout_weight="1"        android:textColor="@color/black"        android:textSize="@dimen/text_size_13"/></LinearLayout>

       每一行有两个TextView,左边宽度为自适应,右边占据剩下左右的位置,在计算出左边最大宽度后,重新设置左边每一个TextView占用的宽度。

方案6

       方式与1差不多,但是不在继承TextView,而是直接继承View:

public class TypographyView4 extends View {    private Paint leftPaint = new Paint();    private Paint rightPaint = new Paint();    private int fullWidth;    private float textSize;    private JSONArray array;    private int middlePadding = 0;    float maxLeftWidth = 0;    int itemSize = 0;    public TypographyView4(Context context) {        super(context);        init();    }    public TypographyView4(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    public TypographyView4(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init() {        textSize = getResources().getDimensionPixelSize(R.dimen.text_size_13);        leftPaint.setAntiAlias(true);        leftPaint.setTextSize(textSize);        leftPaint.setColor(getResources().getColor(R.color.color_black_999999));        rightPaint.setAntiAlias(true);        rightPaint.setTextSize(textSize);        rightPaint.setColor(getResources().getColor(R.color.color_black));        middlePadding = getResources().getDimensionPixelSize(R.dimen.padding_value);    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        fullWidth = getWidth();// 整个textView的宽度    }    public void setText(JSONArray array) {        this.array = array;        if (array != null) {            try {                int size = itemSize = array.length();                for (int i = 0; i < size; ++i) {                    JSONArray o = (JSONArray) array.get(i);                    String key = o.getString(0);                    String value = o.getString(1);                    if (TextUtils.isEmpty(key) || TextUtils.isEmpty(value)) {                        itemSize--;                        continue;                    }                    float curWidth = leftPaint.measureText(key);                    if (curWidth > maxLeftWidth) {                        maxLeftWidth = curWidth;                    }                }                maxLeftWidth = maxLeftWidth + middlePadding;                invalidate();            } catch (Exception e) {            }        }    }    @Override    protected void onDraw(Canvas canvas) {        if (array == null) {            return;        }        int lineCount = 0;        try {            JSONArray item;            float offsetY;            for (int i = 0; i < itemSize; ++i) {                item = (JSONArray) array.get(i);                offsetY = (lineCount + 1) * textSize;                canvas.drawText(item.getString(0), 0, offsetY, leftPaint);                String value = item.getString(1);                float valueWidth = rightPaint.measureText(value);                if (valueWidth > fullWidth - maxLeftWidth) {// 一行显示不完                    char[] textCharArray = value.toCharArray();                    float charWidth;                    float drawWidth = maxLeftWidth;                    for (int j = 0; j < textCharArray.length; j++) {                        charWidth = rightPaint.measureText(textCharArray, j, 1);                        if (fullWidth - drawWidth < charWidth) {                            lineCount++;                            drawWidth = maxLeftWidth;                            offsetY += textSize;                        }                        canvas.drawText(textCharArray, j, 1, drawWidth, offsetY, rightPaint);                        drawWidth += charWidth;                    }                } else {                    canvas.drawText(value, maxLeftWidth, offsetY, rightPaint);                }                lineCount += 2;            }        } catch (JSONException e) {            e.printStackTrace();        }    }}

       该方案主要继承自View,不再继承TextView,由于在在上述方案中不在调用super,因此TextView已经退化为一个View,因此直接继承View。

总结

       因为左边的宽度不确定,因此所有的方案都进行了同样的一个操作,就是测量了左边显示的最大宽度,后续的操作再根据该宽度进行调整。上述的方案中1,2,3,6都只需用一个View来进行显示,4,5都需要多个View进行显示。
       完整的代码可以在查看链接上进行查看。

2 0