Android知识笔记:特效系列,仿instagram文字排版特效

通过打开开发者选项的应用布局边界,可以看到确实ins的输入框的宽度是动态变化的。

下面是打开应用布局边界后的效果图:

当然,这里可能会引入一个新的问题,那就是输入框的宽度是怎么动态改变的?

好让它刚好能够在字体大小变化的过程中最多可容纳的文本数不变。

这个问题会在下面说,这里先不展开。

每行字体大小是怎么确定的,又是怎样联动变化的?

这个问题一开始想了很久,我觉得如果把这个问题搞明白基本就已经成功一半了。大部分人一开始可能都会很容易陷入局部思维,包括我也一样,一直在纠结每行字体是怎么变化的,但其实应该要从整体考虑,从整体考虑一切都会变得很简单,代码实现上也会变得更加容易,不需要处理各种特殊情况。

具体思路:

遍历每行文本,以适应最大文本宽度算出每行的字体大小,然后以每行的字体大小算出每行行高度,把每行行高度累加得到文本总高度,然后判断文本总高度是否大于最大文本高度,如果大于则按比例缩小每行的字体大小,以缩小每行的行高度,得到新的文本总高度,直到文本总高度小于最大文本高度。

上面的这么大段文字总结起来其实就4个步骤:

  1. 拆行
  2. 按匹配最大宽度计算每行字体大小
  3. 按匹配最大高度计算每行字体大小
  4. 重新调整EditText宽度

知识储备

在动手之前我们需要知道几个相关知识点:

span

span可以使TextView分段显示不同样式的文字。在自动排版中因为每行文字字体大小不一样,所以我们需要为每行文字设置不同的span。

Layout

Layout是一个用于各种文本计算的辅助类,TextView的文字排版布局都是依赖于Layout实现的。因为Layout是完全跟TextView解耦的,所以我们可以构建合适的Layout来帮助我们计算字体大小。

下面是Layout的官方定义:

A base class that manages text layout in visual elements on the screen.

For text that will be edited, use a DynamicLayout, which will be updated as the text changes. For text that will not change, use a StaticLayout.

Layout有几个子类,其中较常用的是DynamicLayout和StaticLayout,按照官方的说法,当你的文本是可编辑的则使用的是DynamicLayout,当你的文本不可编辑那么就使用StaticLayout。

所以说EditText的文本计算工作应该都是交给了DynamicLayout实现。

具体实现

首先我们需要监听文字的输入变化,当文本变化时去计算每行的字体大小,最终渲染到屏幕。监听文本变化的代码如下:

addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
refresh();
}

@Override
public void afterTextChanged(Editable s) {

}
});

一.拆行

监听到文本变化后需要对文本进行拆行,得到每行的文字。我们可以通过Layout实现,代码如下:

String text = layout.getText().toString();
int lineCount = layout.getLineCount();
for (int i = 0; i < lineCount; i++) {
int start = layout.getLineStart(i);
int end = layout.getLineEnd(i);
String rowStr = text.substring(start,end);
}

但是,但是,但是,这里有一个点需要特别注意,不能通过EditText自带的Layout来计算每行文本,不然拿到的每行文本是错误的。

为什么呢?

举个例子:

如下图所示,当你在第二行将要输入“好”时,因为你输入"好"后该行文本宽度已经大于此时EditText的宽度了,所以“好”字会被认为是重启一行,这样你得到的每行文本就是错的了,因为“好”应该显示在第二行才对。

这就涉及到我在思路探究中提到的第一个问题,无论我字体怎么缩小放大,如何保证每行最多可显示的文本都是一样的?

那么如何保证呢?

其实很简单,因为影响到文字自动换行的因素主要就是字体大小和最大文本宽度,那么只要保证这两个因素不变,无论你输入什么文本,都能准确一致的拆分出每一行的文字。

因为EditText的每行字体在变,而且宽度也在变,所以通过EditText自带的Layout算出的每行文本肯定是错误的。

所以,思路应该是这样的,你需要构建一个用于计算的Layout,这个Layout的字体大小和宽度必须是固定不变的,这样它就能够保证每行最多可容纳的文本始终是一样的,这样我就能够准确拆分出每行文本。

前面已经说过EditText的计算工作都是交给DynamicLayout,所以我们需要创建的是DynamicLayout。代码如下

protected Layout buildCalculateLayout(CharSequence text,TextView host){
TextPaint paint = new TextPaint(host.getPaint());
paint.setTextSize(mDefFontSize);
return new DynamicLayout(text,paint, mDefMaxTextWidth,host.getLayout().getAlignment(),host.getLayout().getSpacingMultiplier(),host.getLayout().getSpacingAdd(),host.getIncludeFontPadding());
}

需要注意的是,这里除了字体大小和宽度,其他的参数都需要跟EditText的参数一样。

其中mDefFontSize是一开始定义的一个默认字体大小,mDefMaxTextWidth是EditText在没动态调整宽度前的宽度(需要减去padding)。

这样子每次都是通过自构建的Layout去计算每行的文本,就不需要考虑EditText的字体和宽度的动态变化。

二.按匹配最大宽度计算每行字体大小

搞定了第一步拆行后,其实已经离成功不远了,接下来就是如何确定每行字体大小了。

确定字体大小说简单简单,说难也难,关键是看你有没有想到那个点。比如一开始我一直纠结于每行文字是怎么随着输入文字个数和行数变化动态改变的,陷入了局部细节,搞得自己晕头转向,如果按照这个方向思考我感觉估计是怎么做都搞不定的。

后来,想了两天后还是没搞明白,我就试着换个思维方式,从整体来考虑,接下来就有种恍然大悟的感觉,原来其实没那么难。

首先,有一个规律是很显然的:

每行文字越多,它的字体就越小,文字越少,字体就越大。

那么我就想一开始时你把每行文字的宽度放大到最大文本宽度,算出匹配这个宽度的字体应该多大,这样文字越少的行,字体就越大,文字越多的行,字体就越小,这个不就是符合那个规律吗。

计算文本宽度的代码如下:

float width = paint.measureText(text);

因为需要通过不断更改字体大小,去算出匹配最大宽度的字体,所以为了减少计算量,一开始可以做一个初始字体大小的换算。

当字体大小是mDefFontSize时对应的文本宽度是mDefMaxTextWidth,那么当文本宽度是x时,对应的字体大小是y,因为字体大小和宽度成反比(宽度越小,字体越大),所以y的计算公式就是:

y = mDefMaxTextWidth \ x * mDefFontSize
x = paint.measureText(text);

这样我们就可以得到一个比较接近目标值的字体大小,这时候再去判断此时文本宽度是否匹配最大文本宽度,不等于的话再去改变字体大小,直到文本宽度匹配最大文本宽度为止。

代码如下:

public float calculateMatchWidthSize(Paint paint,String text,int maxWidth){
float textSize = paint.getTextSize();
float width = paint.measureText(text);

if(maxWidth >= width && maxWidth - width <= text.length()){
return textSize;
}

if(width > maxWidth){
textSize = getNarrowFitTextSize(paint,text,maxWidth,1);
}else{
textSize = getZoomFitTextSize(paint,text,maxWidth,1);
}

return textSize;
}

private float getNarrowFitTextSize(Paint paint,String text,int maxWidth,float rate){
float textSize = paint.getTextSize();
textSize -= 1 * rate;
paint.setTextSize(textSize);
float width = paint.measureText(text);
if(maxWidth >= width && maxWidth - width <= text.length()){
return textSize;
}
//结束条件
if(width < maxWidth){
return getZoomFitTextSize(paint,text,maxWidth,rate);
}else{
return getNarrowFitTextSize(paint,text,maxWidth,rate);
}
}

private float getZoomFitTextSize(Paint paint,String text,int maxWidth,float rate){
float textSize = paint.getTextSize();
textSize += 1 * rate;
paint.setTextSize(textSize);
float width = paint.measureText(text);
if(maxWidth >= width && maxWidth - width <= text.length()){
return textSize;
}
//结束条件
if(width < maxWidth){
return getZoomFitTextSize(paint,text,maxWidth,rate);
}else{
return getNarrowFitTextSize(paint,text,maxWidth,rate);
}
}

三.按匹配最大高度计算每行字体大小

按照匹配最大宽度计算出来的字体会很大,导致文本高度很高,这时候就需要再动态调整每行字体大小,直到文本高度匹配最大高度为止。

动态调整字体大小时,每行文字的字体大小需要按比例调整,比如每行字体都调整为原来的0.9倍大小。

计算每行文本高度的代码:

int height = paint.getFontMetricsInt(null);

为了让每行文本高度的累加值等于文本实际总高度,需要设置EditText的边距为0并且去掉文字上下的空白部分。代码如下:

//去掉文本上下空白区域
mHost.setIncludeFontPadding(false);
//不设置行间距
mHost.setLineSpacing(0,1);

为了提高计算速度,采用二分法来动态调整字体大小,代码如下:

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门**

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值