写在前面:原创不易,请不要吝啬你的大拇指,点个赞再走呗。然而贴代码很容易,但那不一定有帮助。本文试图从问题点出发,逐步分解,直到实现最终效果。
目录
Part1
1. 如何实现文本右边渐隐的效果
2.如何判断文本内容的宽度超出了布局宽度
3.如何在不聚焦时显示渐隐效果,聚焦时显示跑马灯效果
4.考虑更好的复用
Part2
1.自定义View代码
2.自定义属性代码
3.Color颜色工具类代码
4.Demo布局代码
5.Demo演示Activity
Part3
特别注意
在Android中,我们知道对于控件TextView,在布局时有时候需要单行文本,但可能会存在文本内容的宽度超出了布局宽度,这个时候就需要我们做一定的兼容,不然可能会显示出不太优雅的UI效果。
针对于此情况——文本内容的宽度超出了布局宽度,你应该知道Android通过ellipsize属性,提供了如下几种解决方式:
基于Android默认的实现方式并不难,可能就跑马灯效果时会有点小坑(比如:需要聚焦,或者右边滚动的时候有渐变问题),不过都很好解决。但是我们今天要实现的UI效果,是需要自定义View的,因为目前Android原生并不直接支持——文本过长时右边渐隐,聚焦时跑马灯效果。UI效果如下图所示(静态+动态):
要实现此UI效果,我们需要思考如下问题:
上面提到的问题,主要设计到Android的View绘制流程、画布Canvas、画笔Paint、着色器Shader以及它的子类LinearGradient。如果你和我一样对上面的内容不是特别清楚,也不会影响你继续阅读这篇文章,因为我会很直白的贴出代码供大佬们使用。下面我们一个一个问题展开来说。
直观来看,这个UI效果感觉上是在文本末尾添加了一层遮罩,颜色越来越浅直到完全透明。所以简单来看,可能很多同学会想到画一层遮罩阴影去实现这个效果。但很遗憾,我会告诉你,是不行的,因为我试过,何况文本背景是透明的,你如何做到阴影遮罩只遮在文本上面,而不至于画一个难看的矩形阴影遮罩?
但这个问题正好引出了正确的解决办法。我们需要的是遮罩效果只作用于文本,那么Android是否有提供一种修改文本的方法,做到实现文本渐隐的效果。答案是,有的,当然除了我提供的这种方式,你可能也会去Google/Baidu看看有没有其他方式。
其实,实现方式是比较简单的,我们只需要改变画笔的着色器就可以做到。即Paint类的setShader(Shader shader)方法:
paint.setShader(gradient);
接下来,我们只需要定义一个Shader就OK:
gradient = new LinearGradient(gradientStart, 0, (float) measuredWidth, 0,
new int[]{ColorUtils.changeAlpha(getCurrentTextColor(), 0xFF), getCurrentTextColor(), Color.TRANSPARENT},
new float[]{0f, gradientRatio, 1f},
Shader.TileMode.CLAMP);
这个的实现是比较容易的,获得文本内容的宽度和布局的宽度,比较一下就OK。可以通过Paint类的measureText(String text)测量文本内容宽度,getText()获取文本内容,getMeasuredWidth()获取布局宽度。如下代码所示:
paint.measureText((String) getText()) > getMeasuredWidth()
那就需要设置焦点监听,进而改变文本的UI效果,如下代码所示:
TCLTextView tvHide = findViewById(R.id.tv_element_text_view_long_hide);
tvHide.setEllipsize(null);
tvHide.setOnFocusChangeListener((view, focus) -> {
if (focus) {
tvHide.setEllipsize(TextUtils.TruncateAt.MARQUEE);
tvHide.setTextGradient(false);
} else {
tvHide.setEllipsize(null);
tvHide.setTextGradient(true);
}
});
下面我们一起来回顾下,如何实现此UI效果:
首先,我们需要自定义一个View——TCLTextView,并继承TextView。
然后,需要重写onSizeChanged和onDraw方法。在onSizeChanged方法里面初始化LinearGradient,并拿到默认的文本画笔TextPaint和默认的着色器Shader。在onDraw方法里面判断是否显示渐隐效果——通过一个自定义属性提供出来,并判断文本内容的宽度是否超出了布局宽度,以及获取焦点时设置自定义的Shader、未聚焦时还原Shader。
最后,对于是否显示渐隐效果,还需要提供API出来使用。我们定义了set和get方法,并在set时调用postInvalidate方法,从而重新走onDraw回调。下面贴上代码供大佬们使用。
public class TCLTextView extends TextView {
private TextPaint paint; // 文本画笔
private Shader shader; // 默认Shader
private LinearGradient gradient; // 文本过长渐隐效果
private boolean isTextGradient = false; // 默认没有渐隐效果
public TCLTextView(Context context) {
this(context, null);
}
public TCLTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TCLTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initTextGradient(context, attrs);
// ...
}
@SuppressLint("NewApi")
public TCLTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initTextGradient(context, attrs);
// ...
}
@Override
protected void onSizeChanged(int w, int h, int oldW, int oldH) {
super.onSizeChanged(w, h, oldW, oldH);
int measuredWidth = getMeasuredWidth();
if (measuredWidth > 0) {
float gradientRatio = 2 / 3f; // 渐隐比例,默认从2/3位置开始,可修改
float gradientStart = measuredWidth * gradientRatio; // 渐隐开始位置
gradient = new LinearGradient(gradientStart, 0, (float) measuredWidth, 0,
new int[]{ColorUtils.changeAlpha(getCurrentTextColor(), 0xFF), getCurrentTextColor(), Color.TRANSPARENT},
new float[]{0f, gradientRatio, 1f},
Shader.TileMode.CLAMP); // LinearGradient效果是受文本颜色透明度影响的,于是做出兼容措施。从左到右按比例:无透明文本颜色@0,文本颜色@gradientRatio,完全透明文本颜色@1。
paint = getPaint();
shader = paint.getShader();
}
}
@Override
protected void onDraw(Canvas canvas) {
if (paint != null && gradient != null) {
if (isTextGradient && paint.measureText((String) getText()) > getMeasuredWidth()) {
paint.setShader(gradient);
} else {
paint.setShader(shader);
}
}
super.onDraw(canvas);
}
private void initTextGradient(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TCLTextView);
isTextGradient = typedArray.getBoolean(R.styleable.TCLTextView_ElementNeedTextGradient, false);
typedArray.recycle();
}
public boolean isTextGradient() {
return isTextGradient;
}
public void setTextGradient(boolean textGradient) {
if (textGradient != isTextGradient) {
isTextGradient = textGradient;
postInvalidate();
}
}
}
多说一句,害怕和我一样的小白不知道在哪自定义属性——存放在res--values的attrs.xml文件中,如果没有此文件就创建一个。
public class ColorUtils {
/**
* 修改颜色透明度
* @param color 颜色
* @param alpha 需要修改成的透明橙
* @return 修改透明度后的颜色
*/
public static int changeAlpha(int color, int alpha) {
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
return Color.argb(alpha, red, green, blue);
}
}
a、activity_demo_text_view_long_hide.xml文件。
b、styles.xml文件。
public class TextViewLongHideDemoActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo_text_view_long_hide);
TCLTextView tvHideNo = findViewById(R.id.tv_element_text_view_long_hide_no);
tvHideNo.setEllipsize(null);
tvHideNo.setOnFocusChangeListener((view, focus) -> {
if (focus) {
tvHideNo.setEllipsize(TextUtils.TruncateAt.MARQUEE);
} else {
tvHideNo.setEllipsize(null);
}
});
TCLTextView tvHide = findViewById(R.id.tv_element_text_view_long_hide);
tvHide.setEllipsize(null);
tvHide.setOnFocusChangeListener((view, focus) -> {
if (focus) {
tvHide.setEllipsize(TextUtils.TruncateAt.MARQUEE);
tvHide.setTextGradient(false);
} else {
tvHide.setEllipsize(null);
tvHide.setTextGradient(true);
}
});
}
}
其实这个UI效果的实现比较简单的,核心在setShader这个方法,当你不知道Android提供了此API你可能会一筹莫展,但一旦你获取到了这方面的知识,那应该可以触类旁通,利用LinearGradient去实现更多绚丽的UI,关于LinearGradient的使用可以参考这篇文章的介绍——关于着色器LinearGradient的使用。
这其中其实有个坑,TextView的color如果带有透明度,未聚焦时文本的透明度会降低,聚焦时正常,LinearGradient的构造函数就需要使用文中介绍的带数组的方式;如果不带透明度,那么直接使用startColor和endColor的那个构造函数即可。即如下方式:
gradient = new LinearGradient(0, 0, (float) measuredWidth, 0,
new int[]{getCurrentTextColor(), Color.TRANSPARENT},
new float[]{gradientRatio, 1f},
Shader.TileMode.CLAMP);
这个坑,让我啃了一天,印象深刻,篇幅原因就不在此详述。
——书山有路勤为径,学海无涯苦作舟。