赞
踩
安卓中最长使用的控件就是TextView
,一般而言,使用时只是简单的设置文字,大小,颜色,尺寸。稍微复杂一些的,我们使用Span
标签,Drawable***
等富文本。可能为了显示效果,还会进行padding
,margin
调整,以及 跑马灯 效果的展示。
有时我们可能需要设置文字的行间隔,于是就用了lineSpacingExtra
, lineSpacingMultiplier
;
有时想设置文字拉伸效果,于是会配置textScaleX
或者更进一步,想要修改文本间隔,这时需要设置letterSpacing
。。。
类似的情况还有很多,不过这些不是目前考虑的重点,事实上以上的内容虽然比较繁琐(单从上万行的 TextView 源码便可以看出该控件的功能性),但大多有规律可循,因此并不难使用。
比较麻烦的是,平时进行开发时,我们参照的UI设计图(一般为ps原件)一般都是这个样子 的 :
可以直接测量出两个TextView
之间的间距为33dp,但在实际开发中,这里是无法照抄该尺寸的,TextView
在绘制文字的时候,会自带一些padding效果,因此按照33dp显示出来的效果将会比设计图上大的多。
通常情况,开发人员在指定间隔时,会采用比该值小一些的,比较规范的间隔,比如常用的一些间隔尺寸:16dp
、32dp
、48dp
、64dp
、24dp
,虽然大体上效果类似,但不免有些‘碓答案’
的嫌疑。
如果要明白其中的猫腻,就需要去了解一些文字的显示方式了
在自定义View控件时,都会了解到Canvas
的存在,我们看到的效果都是直接在画布上draw
出来的,文字也不例外。
TextView中会保持一个画笔:Paint
,我们可以在任何地方拿到该对象
TextPaint paint = tv.getPaint();
TextPaint
是Paint
的子类,文本的显示等效果都是由该画笔
在Canvas
上勾勒出来的,绘制文字的方法有这些:
无论哪个重载的方法,都需要传入一个坐标值:(x,y)
这个坐标是文字绘制的起点,但并不是文字左下角的坐标,已TextPaint
定义的规则来看的话,大概是这个样子:
这里列举了多种类型的文本信息包括大小写,上标,表情等;图中显示出了五条线,限定了文字绘制的模版,代表的含义可以查看 FontMetrics类
源码:
// paint为TextPaint类的实例
val fontMetrics = paint.fontMetrics
public static class FontMetrics { /** * The maximum distance above the baseline for the tallest glyph in * the font at a given text size. */ public float top; /** * The recommended distance above the baseline for singled spaced text. */ public float ascent; /** * The recommended distance below the baseline for singled spaced text. */ public float descent; /** * The maximum distance below the baseline for the lowest glyph in * the font at a given text size. */ public float bottom; /** * The recommended additional space to add between lines of text. */ public float leading; }
为了直观的看到效果,我们将TextView
的padding
和margin
值设置 为0dp
,设置文字大小为100sp
,然后对比着说明FontMetrics类
中各字段的含义:
注:需要注意的是
这里所有的取值,都是在TextView为默认样式下获取的
该TextView的宽高为:width: 1280 || height: 266
有了上面的具体数据,我们不难发现一些事实:
* top 和 bottom 之间距离,正好等于 TextView 的高度 ;(可能会有数dp的偏差,这个是由于绘制时,小数向上或向下取值导致的)
* ascent 和 descent 之间的距离,正好是文字内容可达的最大高度;(但从上面可以看出,不同的字符占据的位置是不同的,很多都没有达到最大高度,这个可以参考最后给出的几张包含表情符号的图)
* leading和baseline都是指基准线,也是某些情况下的baseline,我们使用 ConstraintLayout 约束布局时,其中会有 layout_constraintBaseline_toBaselineOf 属性,可以指定两个TextView的基准线对齐
在TextView
中,还有这样的一个属性:
android:includeFontPadding="true"
该属性为false
时,TextView的高度就变成了 ascent 和 descent 之间的距离
上面的结果正好验证了我们之前的结论,即:
当然,这一切的前提是没有drawable和padding
等影响因素的存在。在知道了文字所处的 空间 规则,我们需要怎么来处理 PS图 与 实际效果 的差别呢?
在做尝试之前,需要先明确一些事实:
我们进行UI布局时,情况要比简单的基本布局复杂的多,因此这里做一些限制,在一些
既定使用
的情况下,来完成目标的设定;
这些既定使用
的规则为:
我们姑且按照上面的规则严格执行,当然,这些条件相当苛刻,不过这里只是寻求一种解,并不考虑所有的情况。注:事实上考虑所有的情况将相当复杂,可能逻辑将无法进行下去
好了,在做出了如此多限定后,这里将对TextView进行简单的改造,首先,我们需要获取到原始的 topMargin
值:
override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
super.setLayoutParams(params)
(layoutParams as ViewGroup.MarginLayoutParams?)?.let {
originMarginTop = it.topMargin
originMarginBottom = it.bottomMargin
}
}
该值在设定后,不要轻易改变,不然可能无法还原回去;
然后我们在 TextView 添加到 父ViewGroup之后,去动态的改变 topMargin
,使之满足我们的需要:
override fun hasFocus(): Boolean { if (firstInvokeHasFocus) { (parent is LinearLayout && (parent as LinearLayout).orientation == LinearLayout.VERTICAL).let { //有父类的情况下,查看当前是否有includePadding if (includeFontPadding) { //判断自身的marginTop值是否存在 if (originMarginTop != 0) { //如果存在,则进行缩减,缩减的尺寸为: 自身 (ascent - top) + 上层布局(若为ActualTextView且includeFontPadding为true的话)(bottom - descent) var delete = paint.fontMetrics.ascent - paint.fontMetrics.top val position = (parent as ViewGroup).indexOfChild(this@ActualTextView) val before: View? = (parent as ViewGroup).getChildAt(position - 1) if (before is ActualTextView && before.includeFontPadding) { delete += before.paint.fontMetrics.bottom - before.paint.fontMetrics.descent } //为当前的marginTop赋值 (layoutParams as LinearLayout.LayoutParams).topMargin = (originMarginTop - delete).toInt().also { if (it < 0) 0 else it } } } } firstInvokeHasFocus = false } return super.hasFocus() }
逻辑很简单,就是将上个兄弟View的底部多余部分,与当前View的顶部多余部分
给减去。
这里将有一种意外情况出现:我们需要减去的部分,比设置的margin值还要大
在字体比较大,margin比较小时,这种情况是很容易出现的,不过还好,LinearLayout
布局允许设置负的margin值
,这也是上面要做出诸多限制的原因。
为了显示效果清晰一些,我们加上一些带透明度的背景。
六个TextView对应的topmargin
分别为:0dp,20dp、10dp、5dp、1dp、0dp
在经过处理后,margin 值对应的变成了:
09-30 15:23:12.864 3021-3021/com.example.test.tv E/topMargin::::: -12
09-30 15:23:12.866 3021-3021/com.example.test.tv E/topMargin::::: 24
09-30 15:23:12.868 3021-3021/com.example.test.tv E/topMargin::::: 4
09-30 15:23:12.870 3021-3021/com.example.test.tv E/topMargin::::: -5
09-30 15:23:12.872 3021-3021/com.example.test.tv E/topMargin::::: -13
09-30 15:23:12.874 3021-3021/com.example.test.tv E/topMargin::::: -15
去除背景,添加5条line,查看布局情况:
可以看到倒数第二条绿线比较粗,这是因为在设置了 top-margin为0dp
的情况下,上个 TextView 的 descent
与 当前 TextView 的 ascent
重合在了一起。
如此一来,就完成了既定规则下,设计图与效果图的统一,不过如果需要适应多种情况,则需要真正使用时,进行专门的定制了。这里贴上之前测试使用的自定义TextView
的源码:
/** * function : 包含margin的TextView * * Created on 2018/9/29 18:35 * @author mnlin */ class ActualTextView : AppCompatTextView { /** * 距离的原始高度 */ private var originMarginTop: Int = 0 private var originMarginBottom: Int = 0 /** * 第一次调用hasFocus方法 */ var firstInvokeHasFocus = true constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initPlugins() } /** * 初始化插件类 */ private fun initPlugins() { setSingleLine() ellipsize = TextUtils.TruncateAt.MARQUEE marqueeRepeatLimit = -1 } override fun setLayoutParams(params: ViewGroup.LayoutParams?) { super.setLayoutParams(params) (layoutParams as ViewGroup.MarginLayoutParams?)?.let { originMarginTop = it.topMargin originMarginBottom = it.bottomMargin } } override fun hasFocus(): Boolean { if (firstInvokeHasFocus) { (parent is LinearLayout && (parent as LinearLayout).orientation == LinearLayout.VERTICAL).let { //有父类的情况下,查看当前是否有includePadding if (includeFontPadding) { //判断自身的marginTop值是否存在 if (originMarginTop != 0 || true) { //如果存在,则进行缩减,缩减的尺寸为: 自身 (ascent - top) + 上层布局(若为ActualTextView且includeFontPadding为true的话)(bottom - descent) var delete = paint.fontMetrics.ascent - paint.fontMetrics.top val position = (parent as ViewGroup).indexOfChild(this@ActualTextView) val before: View? = (parent as ViewGroup).getChildAt(position - 1) if (before is ActualTextView && before.includeFontPadding) { delete += before.paint.fontMetrics.bottom - before.paint.fontMetrics.descent } //为当前的marginTop赋值 (layoutParams as LinearLayout.LayoutParams).topMargin = (originMarginTop - delete).toInt().also { Log.e("topMargin::::", it.toString()) if (it < 0) 0 else it } } } } firstInvokeHasFocus = false } return super.hasFocus() } override fun isFocused(): Boolean { return true } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val measuredWidth = measuredWidth val paint = paint val fontMetrics = paint.fontMetrics val top = if (includeFontPadding) fontMetrics.top else fontMetrics.ascent paint.color = Color.BLACK canvas.drawLine(0f, fontMetrics.leading - top, measuredWidth.toFloat(), fontMetrics.leading - top, paint) paint.color = Color.RED canvas.drawLine(0f, fontMetrics.top - top, measuredWidth.toFloat(), fontMetrics.top - top, paint) canvas.drawLine(0f, fontMetrics.bottom - top, measuredWidth.toFloat(), fontMetrics.bottom - top, paint) paint.color = Color.GREEN canvas.drawLine(0f, fontMetrics.ascent - top, measuredWidth.toFloat(), fontMetrics.ascent - top, paint) canvas.drawLine(0f, fontMetrics.descent - top, measuredWidth.toFloat(), fontMetrics.descent - top, paint) paint.color = Color.BLUE canvas.drawLine(100f, paint.baselineShift - top, measuredWidth.toFloat(), paint.baselineShift - top, paint) Log.e("fontMetrics::::", "top:${fontMetrics.top} ; ascent:${fontMetrics.ascent} ; leading:${fontMetrics.leading} ; descent:${fontMetrics.descent} ; bottom:${fontMetrics.bottom}") Log.e("width: || height: ", width.toString() + " || " + height) } }
以及其中使用到的复杂的表情等:
<string name="common_test">乱2JYjy²?麣?√∈✔₎</string>
最后使用的布局文件xml:
<?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:gravity="center" android:orientation="vertical" tools:context=".MainActivity"> <!--android:background="#00005555"--> <com.example.test.tv.ActualTextView android:id="@+id/tv_one" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_margin="0dp" android:background="#00005555" android:includeFontPadding="true" android:padding="0dp" android:singleLine="true" android:text="@string/common_test" android:textSize="50sp"/> <!--android:background="#00555500"--> <com.example.test.tv.ActualTextView android:id="@+id/tv_two" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="20dp" android:background="#00555500" android:includeFontPadding="true" android:letterSpacing="0" android:padding="0dp" android:singleLine="true" android:text="@string/common_test" android:textSize="50sp"/> <com.example.test.tv.ActualTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="10dp" android:background="#00005555" android:includeFontPadding="true" android:letterSpacing="0" android:padding="0dp" android:singleLine="true" android:text="@string/common_test" android:textSize="50sp"/> <com.example.test.tv.ActualTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="5dp" android:background="#00555500" android:includeFontPadding="true" android:letterSpacing="0" android:padding="0dp" android:singleLine="true" android:text="@string/common_test" android:textSize="50sp"/> <com.example.test.tv.ActualTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="1dp" android:background="#00005555" android:includeFontPadding="true" android:letterSpacing="0" android:padding="0dp" android:singleLine="true" android:text="@string/common_test" android:textSize="50sp"/> <com.example.test.tv.ActualTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="0dp" android:background="#00555500" android:includeFontPadding="true" android:letterSpacing="0" android:padding="0dp" android:singleLine="true" android:text="@string/common_test" android:textSize="50sp"/> </LinearLayout>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。