今天来一起学习一下最简单的自定义view,自己动手写一个MyTextView,当然不会像系统的TextView那么复杂,只是实现一下TextView的简单功能,包括分行显示及自定义属性的处理,主要目的是介绍自定义view的实现的基本思路和需要掌握的一些基础知识。

《一》先展示一下实现的最终效果

image.png

《二》实现步骤分析

1、创建MyTextView extends View,重写构造方法。一般是重写前三个构造方法,让前两个构造方法最终调用三个参数的构造方法,然后在第三个构造方法中进行一些初始化操作。

2、在构造方法中进行一些初始化操作,如初始化画笔及获取自定义属性等。

如何自定义属性?

(1)在values下创建attrs.xml.

//定义你的view可以在布局文件中配置的自定义属性

(2)获取自定义属性

TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextViewApprence, defStyleAttr, 0);

mText = typedArray.getString(R.styleable.MyTextViewApprence_text);

mTextColor = typedArray.getColor(R.styleable.MyTextViewApprence_textColor, Color.BLACK);

mTextSize = (int) typedArray.getDimension(R.styleable.MyTextViewApprence_textSize, 15);

showMode = typedArray.getInt(R.styleable.MyTextViewApprence_showMode, 0);

typedArray.recycle();

3、重写OnDraw()方法,在onDraw()中使用canvas绘制文字,x,y为绘制的起点。

需要注意两点:

(1)这里的x,y不是指的左上顶点,而是左下顶点。

(2)drawText绘制文字时,是有规则的,这个规则就是基线!详细可阅读drawText()详解

image.png

//绘制每行文字的建议高度为:

Paint.FontMetrics fm = mPaint.getFontMetrics();

drawTextHeight = (int) (fm.descent - fm.ascent);

绘制文字的方法:

canvas.drawText(@NonNull String text, float x, float y, @NonNull Paint paint)

4、到第三步,其实就可以绘制出文字了,但是会发现一个问题,无论在布局文件中声明控件的宽高是wrap_content和match_parent,效果都是铺满了整个屏幕,这个时候,我们就需要重写onMesure()方法来测量控件的实际大小了,分析View的源码:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

public static int getDefaultSize(int size, int measureSpec) {

int result = size;

int specMode = MeasureSpec.getMode(measureSpec);

int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {

case MeasureSpec.UNSPECIFIED:

result = size;

break;

case MeasureSpec.AT_MOST:

case MeasureSpec.EXACTLY:

result = specSize;

break;

}

return result;

}

/**MeasureSpec 封装了父控件对其孩子的布局要求

有大小和模式两种,而模式则有三种模式

public static class MeasureSpec {

private static final int MODE_SHIFT = 30;

private static final int MODE_MASK = 0x3 << MODE_SHIFT;

//父控件不强加任何约束给子控件,它可以为它逍遥的任何大小

public static final int UNSPECIFIED = 0 << MODE_SHIFT; //0

//父控件给子控件一个精确的值

public static final int EXACTLY = 1 << MODE_SHIFT; //1073741824

//父控件给子控件竟可能最大的值

public static final int AT_MOST = 2 << MODE_SHIFT; //-2147483648

//设定尺寸和模式创建的统一约束规范

public static int makeMeasureSpec(int size, int mode) {

if (sUseBrokenMakeMeasureSpec) {

return size + mode;

} else {

return (size & ~MODE_MASK) | (mode & MODE_MASK);

}

}

// 从规范中获取模式

public static int getMode(int measureSpec) {

return (measureSpec & MODE_MASK);

}

//从规范中获取尺寸

public static int getSize(int measureSpec) {

return (measureSpec & ~MODE_MASK);

}

}

关于specMode测量的几种模式,你需要知道它们的作用,如下图。

image.png

可以看到我们的源码中调用是自身的getDefaultSize()方法,然后在MeasureSpec.AT_MOST和MeasureSpec.EXACTLY全部返回的是specSize,而specSize表示的是父控件剩余宽度,也就是我们看到的全屏。所以默认onMeasure方法中wrap_content 和match_parent 的效果是一样的,都是填充剩余的空间。

所以我们重新onMesure()方法,对wrap_content这种情况进行处理。

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

//获取宽的模式

int widthMode = MeasureSpec.getMode(widthMeasureSpec);

int heightMode = MeasureSpec.getMode(heightMeasureSpec);

//获取宽的尺寸

int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSize = MeasureSpec.getSize(heightMeasureSpec);

Log.e("TAG", "widthMode=" + widthMode + " heightMode=" + heightMode + " widthSize=" + widthSize + " heightSize=" + heightSize);

//对wrap_content这种模式进行处理

int width;

int height;

if (widthMode == MeasureSpec.EXACTLY) {

width = widthSize;

} else {

//如果是wrap_content,我们需要得到控件需要多大的尺寸

//首先丈量文本的宽度

float textWidth;

textWidth = mTextBoundOther.width();

//控件的宽度就是文本的宽度加上两边的内边距。内边距就是padding的值,在构造方法执行完被赋值

width = (int) (getPaddingLeft() + textWidth + getPaddingRight());

}

if (heightMode == MeasureSpec.EXACTLY) {

height = heightSize;

} else {

//如果是wrap_content,我们需要得到控件需要多大的尺寸

//首先丈量文本的宽度

float textHeight = mTextBoundOther.height();

//控件的宽度就是文本的宽度加上两边的内边距。内边距就是padding的值,在构造方法执行完被赋值。遗留问题:最后一行显示高度不够,在这里加上10px处理

height = (int) (getPaddingTop() + textHeight + getPaddingBottom() + 10);

}

//保存丈量结果

setMeasuredDimension(width, height);

}

下面是实现了自动换行的TextView的完整代码:

package com.example.jojo.learn.customview;

import android.content.Context;

import android.content.res.TypedArray;

import android.graphics.Canvas;

import android.graphics.Color;

import android.graphics.Paint;

import android.graphics.Rect;

import android.support.annotation.Nullable;

import android.text.TextUtils;

import android.util.AttributeSet;

import android.util.DisplayMetrics;

import android.util.Log;

import android.view.View;

import com.example.jojo.learn.R;

import java.util.ArrayList;

/**

* Created by JoJo on 2018/7/27.

* wechat:18510829974

* description:自定义Textview

*/

public class MyTextView extends View {

//文字内容

private String mText;

//文字大小

private int mTextSize;

//文字颜色

private int mTextColor;

//绘制的范围

private Rect mTextBound;

//绘制文字的画笔

private Paint mPaint;

private int mScreenWidth;

private int mScreenHeight;

private int baseLineY;

private float ascent;

private float descent;

private float top;

private float bottom;

private int baseLineX;

private Rect mMaxRect;

private Rect mTextBoundOther;

private String text = "This is a great day";

private int drawTextHeight;

public MyTextView(Context context) {

this(context, null);

}

public MyTextView(Context context, @Nullable AttributeSet attrs) {

this(context, attrs, 0);

}

public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextViewApprence, defStyleAttr, 0);

mText = typedArray.getString(R.styleable.MyTextViewApprence_text);

mTextColor = typedArray.getColor(R.styleable.MyTextViewApprence_textColor, Color.BLACK);

mTextSize = (int) typedArray.getDimension(R.styleable.MyTextViewApprence_textSize, 15);

showMode = typedArray.getInt(R.styleable.MyTextViewApprence_showMode, 0);

typedArray.recycle();

//屏幕信息

DisplayMetrics dm = getResources().getDisplayMetrics();

mScreenHeight = dm.heightPixels;

mScreenWidth = dm.widthPixels;

if (TextUtils.isEmpty(mText)) {

mText = "Hello.....Hello.....Hello.....Hello.....Hello.....Hello.....Hello.....Hello.....Hello.....Hello.....Hello....";

}

init();

}

private void init() {

//基线

baseLineY = mTextSize;

baseLineX = 0;

//初始化画笔

mPaint = new Paint();

mPaint.setColor(mTextColor);

mPaint.setTextSize(mTextSize);

mPaint.setAntiAlias(true);

mPaint.setStrokeWidth(1);

//获取绘制的宽高

mTextBound = new Rect();

mPaint.getTextBounds(text, 0, text.length(), mTextBound);

mTextBound.top = baseLineY + mTextBound.top;

mTextBound.bottom = baseLineY + mTextBound.bottom;

mTextBound.left = baseLineX + mTextBound.left;

mTextBound.right = baseLineX + mTextBound.right;

//获取文字所占区域最小矩形

Log.e("TAG", mTextBound.toShortString());

//换行的文字

mTextBoundOther = new Rect();

mPaint.getTextBounds(mText, 0, mText.length(), mTextBoundOther);

//计算各线在位置

Paint.FontMetrics fm = mPaint.getFontMetrics();

ascent = baseLineY + fm.ascent;//当前绘制顶线

descent = baseLineY + fm.descent;//当前绘制底线

top = baseLineY + fm.top;//可绘制最顶线

bottom = baseLineY + fm.bottom;//可绘制最低线

//每行文字的绘制高度

drawTextHeight = (int) (fm.descent - fm.ascent);

//字符串所占的高度和宽度

int width = (int) mPaint.measureText(mText);

int height = (int) (bottom - top);

//文字绘制时可以占据的最大矩形区域

mMaxRect = new Rect(baseLineX, (int) (baseLineY + fm.top), (baseLineX + width), (int) (baseLineY + fm.bottom));

}

private ArrayList mTextList = new ArrayList<>();

private float lineNum;//文字最终所占的行数

private float spLineNum;

//换行展示的对齐方式

private int showMode;

/**

* 测量

* 父控件不强加任何约束给子控件,它可以为它逍遥的任何大小

* public static final int UNSPECIFIED = 0 << MODE_SHIFT; //0

* 父控件给子控件一个精确的值-match_parent

* public static final int EXACTLY = 1 << MODE_SHIFT; //1073741824

* 父控件给子控件竟可能最大的值-wrap_content

* public static final int AT_MOST = 2 << MODE_SHIFT; //-2147483648

*

* @param widthMeasureSpec

* @param heightMeasureSpec

*/

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

//获取宽的模式

int widthMode = MeasureSpec.getMode(widthMeasureSpec);

int heightMode = MeasureSpec.getMode(heightMeasureSpec);

//获取宽的尺寸

int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSize = MeasureSpec.getSize(heightMeasureSpec);

Log.e("TAG", "widthMode=" + widthMode + " heightMode=" + heightMode + " widthSize=" + widthSize + " heightSize=" + heightSize);

//(1)实现文字自动换行显示

//文字的宽度

int mTextWidth = mTextBoundOther.width();

if (mTextList.size() == 0) {

//将文本分段

int padding = getPaddingLeft() + getPaddingRight();

int specMaxWidth = widthSize - padding;//可显示文本的最大宽度

//最大宽度大于文字所占宽度,则一行就能显示完全

if (specMaxWidth >= mTextWidth) {

lineNum = 1;

mTextList.add(mText);

} else {

//超过一行,需切割,分行显示

spLineNum = mTextWidth * 1.0f / specMaxWidth;

//如果有小数的话就进1

if ((spLineNum + "").contains(".")) {

lineNum = (float) (spLineNum + 0.5);

} else {

lineNum = spLineNum;

}

//每行展示的文字的长度

int lineLength = (int) (mText.length() / spLineNum);

for (int i = 0; i < lineNum; i++) {

String lineStr;

//判断是否可以一行展示

if (mText.length() < lineLength) {

lineStr = mText.substring(0, mText.length());

} else {

lineStr = mText.substring(0, lineLength);

}

mTextList.add(lineStr);

//内容切割完,记录切割后的字符串,重新赋值给mText

if (!TextUtils.isEmpty(mText)) {

if (mText.length() < lineLength) {

mText = mText.substring(0, mText.length());

} else {

mText = mText.substring(lineLength, mText.length());

}

} else {

break;

}

}

}

}

//(2)下面对wrap_content这种模式进行处理

int width;

int height;

if (widthMode == MeasureSpec.EXACTLY) {

width = widthSize;

} else {

//如果是wrap_content,我们需要得到控件需要多大的尺寸

//首先丈量文本的宽度

float textWidth;

if (mTextList.size() > 1) {

textWidth = widthSize;

} else {

textWidth = mTextBoundOther.width();

}

//控件的宽度就是文本的宽度加上两边的内边距。内边距就是padding的值,在构造方法执行完被赋值

width = (int) (getPaddingLeft() + textWidth + getPaddingRight());

}

if (heightMode == MeasureSpec.EXACTLY) {

height = heightSize;

} else {

//如果是wrap_content,我们需要得到控件需要多大的尺寸

//首先丈量文本的宽度

// float textHeight = mTextBoundOther.height();

float textHeight = drawTextHeight * mTextList.size();

//控件的宽度就是文本的宽度加上两边的内边距。内边距就是padding的值,在构造方法执行完被赋值。遗留问题:最后一行显示高度不够,在这里加上10px处理

height = (int) (getPaddingTop() + textHeight + getPaddingBottom() + 10);

}

//保存丈量结果

setMeasuredDimension(width, height);

}

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

/**

* 测试文字的绘制区域

*/

// //绘制字符串所占的矩形区域

// mPaint.setColor(Color.GREEN);

// canvas.drawRect(mMaxRect, mPaint);

//

// //绘制最小矩形

// mPaint.setColor(Color.RED);

// canvas.drawRect(mTextBound, mPaint);

//

// //绘制文字-绘制的起点是:绘制文字所在矩形的左下角顶点

// mPaint.setColor(Color.WHITE);

// canvas.drawText(text, baseLineX, baseLineY, mPaint);

//

// //绘制基线

// mPaint.setColor(Color.RED);

// canvas.drawLine(0, baseLineY, mScreenWidth, baseLineY, mPaint);

//

// mPaint.setColor(Color.YELLOW);

// canvas.drawLine(0, top, mScreenWidth, top, mPaint);

// mPaint.setColor(Color.GREEN);

// canvas.drawLine(0, ascent, mScreenWidth, ascent, mPaint);

// mPaint.setColor(Color.BLACK);

// canvas.drawLine(0, descent, mScreenWidth, descent, mPaint);

// mPaint.setColor(Color.WHITE);

// canvas.drawLine(0, bottom, mScreenWidth, bottom, mPaint);

// 绘制Hello World !

// canvas.drawText(text, getWidth() / 2 - mTextBoundOther.width() / 2, getHeight() / 2 + mTextBoundOther.height() / 2, mPaint);

//分行绘制文字

for (int i = 0; i < mTextList.size(); i++) {

mPaint.getTextBounds(mTextList.get(i), 0, mTextList.get(i).length(), mTextBoundOther);

//换行左对齐展示

if (showMode == 0) {

canvas.drawText(mTextList.get(i), 0 + getPaddingLeft(), (getPaddingTop() + drawTextHeight * (i + 1)), mPaint);

} else if (showMode == 1) {

//换行居中展示

canvas.drawText(mTextList.get(i), (getWidth() / 2 - mTextBoundOther.width() / 2) + getPaddingLeft(), (getPaddingTop() + drawTextHeight * (i + 1)), mPaint);

}

}

}

/**

* 控制文字对齐方式:居中或者居左

*

* @param showMode

*/

public void reLayoutText(int showMode) {

this.showMode = showMode;

invalidate();

}

}

涉及到的自定义属性:attrs.xml中

在布局文件中使用,测试代码:

xmlns:mytext="http://schemas.android.com/apk/res-auto"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:background="@color/colorAccent">

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@android:color/white"

android:onClick="textLayoutLeft"

android:text="文字居左对齐" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_alignParentRight="true"

android:background="@android:color/white"

android:onClick="textLayoutCenter"

android:text="文字居中对齐" />

android:id="@+id/mytextview"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_centerInParent="true"

android:background="@android:color/holo_red_dark"

mytext:showMode="center"

mytext:text="来一碗大的毒鸡汤:无论做什么事情,你首先要想到的不是你能得到什么,而是你能接受失去什么,当你无畏失去什么的时候,你就变得无敌了。人生最重要的不是所站的位置是所站的位置是所站的位置是所站的位置是所站的位置你来自何处并不重要,重要的是你要去往何方,人生最重要的不是所站的位置,而是所去的方向。人只要不失去方向,就永远不会失去自己!无论做什么事情,你首先要想到的不是你能得到什么,而是你能接受失去什么,当你无畏失去什么的时候,你就变得无敌了"

mytext:textColor="@android:color/white"

mytext:textSize="50px" />

欢迎各位读者一起来探索下面的待解决的问题:

1、中英文混排时展示有问题

2、最后一行测量给的高度不够,导致最后一行展示不全

3、textSize的单位,在布局文件中没有处理成sp,而是px。如果需要处理成sp,可以参考系统TextView源码。

image.png

可以参考如下处理方式:

mTextSize = (int) typedArray.getDimension(R.styleable.MyTextViewApprence_textSize, sp2px(mTextSize));

/**

* 将sp转换成px

*

* @param sp

* @return

*/

private int sp2px(int sp) {

return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,

getResources().getDisplayMetrics());

}

android 换行模式,Android进阶之自定义View(1)实现可换行的TextView相关推荐

  1. Android进阶之自定义View实战(二)九宫格手势解锁实现

    一.引言 在上篇博客Android进阶之自定义View实战(一)仿iOS UISwitch控件实现中我们主要介绍了自定义View的最基本的实现方法.作为自定义View的入门篇,仅仅介绍了Canvas的 ...

  2. Android安卓仿IOS音量调节-自定义view系列(4)

    Android安卓仿IOS音量调节-自定义view系列 功能简介 主要实现步骤 xml相关属性设置 java代码 Android技术生活交流 更多其他页面-自定义View-实用功能合集:点击查看 Gi ...

  3. Android仿IOS解锁密码界面-自定义view系列(6)

    Android仿IOS解锁密码界面-自定义view系列 功能简介 主要实现步骤-具体内容看github项目里的代码 xml相关属性设置 Android Studio 代码 Android技术生活交流 ...

  4. Android开发之制作圆形头像自定义View,直接引用工具类,加快开发速度。带有源代码学习

    作者:程序员小冰,CSDN博客:http://blog.csdn.net/qq_21376985 QQ986945193 博客园主页:http://www.cnblogs.com/mcxiaobing ...

  5. Android开发之制作圆形头像自定义View,直接引用工具类,加快开发速度。带有源代码学习...

    作者:程序员小冰,CSDN博客:http://blog.csdn.net/qq_21376985 QQ986945193 博客园主页:http://www.cnblogs.com/mcxiaobing ...

  6. Android绘图机制(三)——自定义View的实现方式以及半弧圆新控件

    Android绘图机制(三)--自定义View的三种实现方式以及实战项目操作 在Android绘图机制(一)--自定义View的基础属性和方法 里说过,实现自定义View有三种方式,分别是 1.对现有 ...

  7. Android绘图机制(二)——自定义View绘制形, 圆形, 三角形, 扇形, 椭圆, 曲线,文字和图片的坐标讲解

    Android绘图机制(二)--自定义View绘制形, 圆形, 三角形, 扇形, 椭圆, 曲线,文字和图片的坐标讲解 我们要想画好一些炫酷的View,首先我们得知道怎么去画一些基础的图案,比如矩形,圆 ...

  8. HenCoder Android 开发进阶:自定义 View 1-5 绘制顺序

    这期是 HenCoder 自定义绘制的第 1-5 期:绘制顺序 之前的内容在这里:  HenCoder Android 开发进阶 自定义 View 1-1 绘制基础  HenCoder Android ...

  9. Android 高手进阶之自定义View,自定义属性(带进度的圆形进度条)

    转载请注明地址:http://blog.csdn.net/xiaanming/article/details/10298163 很多的时候,系统自带的View满足不了我们功能的需求,那么我们就需要自己 ...

最新文章

  1. DNN和IBatis.Net几乎同时发布新版本
  2. java 算法笔试题_【干货】经典算法面试题代码实现-Java版
  3. 38 | 案例篇:怎么使用 tcpdump 和 Wireshark 分析网络流量?
  4. 外部工具连接SaaS模式云数仓MaxCompute—ETL工具篇
  5. 时隔两年 重新当码农
  6. 数据倾斜?几招把你安排的板板正正的!
  7. MySQL:MySQL基础(一)
  8. ubuntu16.04下Caffe绘制训练过程的loss和accuracy曲线
  9. 图书在线网店的html,网上书城html模板.docx
  10. 细谈等级保护与ISO27000系列的区别与联系
  11. 在windows下制作grub2引导的多启动U盘(可启动wepe,安装centos7,debian10等)
  12. 【PS专题】PS钢笔压力有感叹号怎么消除
  13. VPP协议栈学习一:snd_wnd
  14. 2018年,给自己加油!
  15. java线程之可重入锁
  16. Java去掉字符串首尾的““
  17. lavarvel框架路由
  18. 计算机c盘加容量,增加C盘空间,详细教您怎么增加C盘空间
  19. 红图新媒体-新媒体运营从何入手呢?速点了解更多
  20. < 性能提升 Get √ :如何理解 “ 回流 ” 与 “ 重绘 ” ?如何合理的减少其出现呢 ? >

热门文章

  1. libevent: linux安装libevent
  2. derby数据库操作比较难理解的错误及解决方法大全
  3. 【转】android Notification 的使用
  4. CSS教程--CSS背景
  5. [导入]韩语基本会话
  6. linux-04-磁盘命令+进程命令
  7. 小白 LeetCode 5605 检查两个字符串数据是否相等
  8. sql获取日期相差天数oracle,找到oracle sql中两个日期之间的经过时间
  9. 常见25种深度学习模型的github代码
  10. OpenCV与图像处理学习十六——模板匹配