Android进阶之路 - TextView文本渐变
那天做需求的时候,遇到一个小功能,建立在前人栽树,后人乘凉的情况下,仅用片刻就写完了;说来惭愧,我以前并未写过文本渐变的需求,脑中也仅有一个shape渐变带来的大概思路,回头来看想着学习一下这款自定义控件的内部实现,故记录于此
很多时候通过阅读原作者源码,总能为我们带来一些思考,一些成长
Tip:为表尊重,源码中的注释声明并未做任何修改,仅记录自身学习中的思考、想法
- 效果
- 需求效果
- 实现效果
- 基础思考
- 开发实践
- 项目结构
- 使用方式
- 集成学习
- ShapeTextView 自定义控件
- shape_attr 自定义属性
- Styleable 动态属性
- IShapeDrawableStyleable 背景属性抽象类
- ITextColorStyleable 文本属性抽象类
- ShapeTextViewStyleable 具体实现类
- Builder
- ShapeDrawableBuilder
- TextColorBuilder
- LinearGradientFontSpan 文本渐变核心类
效果
可以先看看效果是不是你所需要的,以免浪费开发时间… 如有需要可直接 下载Demo
需求效果
渐变背景
渐变文本
实现效果
本文只针对item样式中的 右上角的双重渐变(渐变背景、渐变文本)实现
基础思考
如果让你实现右上角的标签,你考虑了哪些实现方式?
Tip:右上角标签仅可能有一个,只是样式、描述不同
- 单标签固定样式:产品要求不严格的话,直接让设计切图!(简单便捷)
- 多标签固定样式:产品要求不严格的话,当样式标签固定在 2-5个之间,可以让设计切图,显示逻辑处理!(简单便捷)
- 多标签不固定样式:例如内部可能字体可能是精选、常态、盈利等等,当这种场景被不确定占据时,只能通过代码来进行适配
关于渐变背景,因仅为一种固定渐变背景,且并非本文关键,故在此直接写出,如果你想更详细的学习和了解shape,可前往 shape保姆级手册
关于渐变位置主要有startColor、centerColor、endColor ,如根据设计图的话,可仅设置startColor、endColor
shape_staid_select_top_right(背景shape)- Demo中可能未打包
开发实践
因为我们的文本渐变效果是直接从 ShapeView 剥离的产物,所以如果不介意引入三方库的话,最简单的方式肯定是直接依赖了,只不过会导致项目体积更大一些,毕竟有些东西我们用不到
关于 ShapeView 的集成,可自行前往作者项目主页查看,此处仅做部分摘要
项目结构
此处项目结构为我Demo的结构目录,主要通过减少三方库的引入,从而减少项目体积、包体积
使用方式
ShapeTextView 就是我们这次学习的控件,现在开始一步步倒推看一下
集成学习
ShapeTextView 自定义控件
我感觉自定义控件内主要有以下几点,需要总结、注意
- 初始化背景属性、文本属性(内部)
- 通过读取typefaceScale属性,设置对应字体加粗效果(内部)
- 增添动态设置Text、setTextColor、TypefaceScale方法(支持外部调用)
package com.example.shapefontbg.shape import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView import com.example.shapefontbg.R import com.example.shapefontbg.shape.builder.ShapeDrawableBuilder import com.example.shapefontbg.shape.builder.TextColorBuilder import com.example.shapefontbg.shape.styleable.ShapeTextViewStyleable /** * author : Android 轮子哥 * github : https://github.com/getActivity/ShapeView * time : 2021/07/17 * desc : 支持直接定义 Shape 背景的 TextView */ class ShapeTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatTextView(context, attrs, defStyleAttr) { private val shapeDrawableBuilder: ShapeDrawableBuilder private val textColorBuilder: TextColorBuilder? private var typefaceScale: Float companion object { private val STYLEABLE = ShapeTextViewStyleable() } enum class TypefaceScale { MEDIUM, MEDIUM_SMALL, DEFAULT, } init { val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ShapeTextView) //初始化背景属性 shapeDrawableBuilder = ShapeDrawableBuilder(this, typedArray, STYLEABLE) //初始化文本属性 textColorBuilder = TextColorBuilder(this, typedArray, STYLEABLE) // 读取字体加粗程度 val scale = typedArray.getInt(R.styleable.ShapeTextView_typefaceScale, 0) typefaceScale = typedArrayTypefaceScale(scale) // 资源回收 typedArray.recycle() // 设置相关背景属性 shapeDrawableBuilder.intoBackground() // 设置相关文本属性 // 原始部分 // if (textColorBuilder.isTextGradientColors) { // text = textColorBuilder.buildLinearGradientSpannable(text) // } else { // textColorBuilder.intoTextColor() // } textColorBuilder.intoTextColor() } /** * 字体粗度 * */ private fun typedArrayTypefaceScale(scale: Int): Float = when (scale) { 1 -> 0.6f 2 -> 1.1f else -> 0.0f } override fun setTextColor(color: Int) { super.setTextColor(color) textColorBuilder?.textColor = color textColorBuilder?.clearTextGradientColors() } /** * 渐变入口 * */ override fun setText(text: CharSequence, type: BufferType) { if (textColorBuilder?.isTextGradientColors == true) { super.setText(textColorBuilder.buildLinearGradientSpannable(text), type) } else { super.setText(text, type) } } override fun onDraw(canvas: Canvas?) { if (typefaceScale == 0f) { return super.onDraw(canvas) } val strokeWidth = paint.strokeWidth val style = paint.style paint.strokeWidth = typefaceScale paint.style = Paint.Style.FILL_AND_STROKE super.onDraw(canvas) paint.strokeWidth = strokeWidth paint.style = style } fun setTypefaceScale(scale: TypefaceScale = TypefaceScale.DEFAULT) { typefaceScale = when (scale) { TypefaceScale.DEFAULT -> 0.0f TypefaceScale.MEDIUM_SMALL -> 0.6f TypefaceScale.MEDIUM -> 1.1f } invalidate() } }
修改部分
原始
if (textColorBuilder.isTextGradientColors) { text = textColorBuilder.buildLinearGradientSpannable(text) } else { textColorBuilder.intoTextColor() }
改为(可能多走了一层内层判断,性能应该没有太大影响)
textColorBuilder.intoTextColor()
原因:内部、外部判断逻辑重复,去除外部判断即可
内部实现为文本渐变核心类,后续会单独说明
shape_attr 自定义属性
Styleable 动态属性
分别针对 ShapeView背景 和 TextView自身 通用型自定义属性
我觉得因为原始项目中具体实现Styleable类有很多个,如果后续有扩展的话,也可以再用工厂模式或策略模式二次封装;
IShapeDrawableStyleable 背景属性抽象类
package com.example.shapefontbg.shape.styleable /** * author : Android 轮子哥 * github : https://github.com/getActivity/ShapeView * time : 2021/08/28 * desc : ShapeDrawable View 属性收集接口 */ interface IShapeDrawableStyleable { val shapeTypeStyleable: Int val shapeWidthStyleable: Int val shapeHeightStyleable: Int val solidColorStyleable: Int val solidPressedColorStyleable: Int val solidCheckedColorStyleable: Int get() = 0 val solidDisabledColorStyleable: Int val solidFocusedColorStyleable: Int val solidSelectedColorStyleable: Int val radiusStyleable: Int val topLeftRadiusStyleable: Int val topRightRadiusStyleable: Int val bottomLeftRadiusStyleable: Int val bottomRightRadiusStyleable: Int val startColorStyleable: Int val centerColorStyleable: Int val endColorStyleable: Int val useLevelStyleable: Int val angleStyleable: Int val gradientTypeStyleable: Int val centerXStyleable: Int val centerYStyleable: Int val gradientRadiusStyleable: Int val strokeColorStyleable: Int val strokePressedColorStyleable: Int val strokeCheckedColorStyleable: Int get() = 0 val strokeDisabledColorStyleable: Int val strokeFocusedColorStyleable: Int val strokeSelectedColorStyleable: Int val strokeWidthStyleable: Int val dashWidthStyleable: Int val dashGapStyleable: Int }
ITextColorStyleable 文本属性抽象类
package com.example.shapefontbg.shape.styleable /** * author : Android 轮子哥 * github : https://github.com/getActivity/ShapeView * time : 2021/08/28 * desc : 文本颜色 View 属性收集接口 */ interface ITextColorStyleable { val textColorStyleable: Int val textPressedColorStyleable: Int val textCheckedColorStyleable: Int get() = 0 val textDisabledColorStyleable: Int val textFocusedColorStyleable: Int val textSelectedColorStyleable: Int val textStartColorStyleable: Int val textCenterColorStyleable: Int val textEndColorStyleable: Int val textGradientOrientationStyleable: Int }
ShapeTextViewStyleable 具体实现类
我感觉主要有俩点作用:
- 声明对应抽象类的自定义属性,可用于固定属性、动态设置属性
- 将动态属性和静态自定义属性做了一个基础映射
package com.example.shapefontbg.shape.styleable import com.example.shapefontbg.R /** * author : Android 轮子哥 * github : https://github.com/getActivity/ShapeView * time : 2021/08/28 * desc : TextView 的 Shape 属性值 */ class ShapeTextViewStyleable : IShapeDrawableStyleable, ITextColorStyleable { /** * [IShapeDrawableStyleable] */ override val shapeTypeStyleable = R.styleable.ShapeTextView_shape override val shapeWidthStyleable = R.styleable.ShapeTextView_shape_width override val shapeHeightStyleable = R.styleable.ShapeTextView_shape_height override val solidColorStyleable = R.styleable.ShapeTextView_shape_solidColor override val solidPressedColorStyleable = R.styleable.ShapeTextView_shape_solidPressedColor override val solidDisabledColorStyleable = R.styleable.ShapeTextView_shape_solidDisabledColor override val solidFocusedColorStyleable = R.styleable.ShapeTextView_shape_solidFocusedColor override val solidSelectedColorStyleable = R.styleable.ShapeTextView_shape_solidSelectedColor override val radiusStyleable = R.styleable.ShapeTextView_shape_radius override val topLeftRadiusStyleable = R.styleable.ShapeTextView_shape_topLeftRadius override val topRightRadiusStyleable = R.styleable.ShapeTextView_shape_topRightRadius override val bottomLeftRadiusStyleable = R.styleable.ShapeTextView_shape_bottomLeftRadius override val bottomRightRadiusStyleable = R.styleable.ShapeTextView_shape_bottomRightRadius override val startColorStyleable = R.styleable.ShapeTextView_shape_startColor override val centerColorStyleable = R.styleable.ShapeTextView_shape_centerColor override val endColorStyleable = R.styleable.ShapeTextView_shape_endColor override val useLevelStyleable = R.styleable.ShapeTextView_shape_useLevel override val angleStyleable = R.styleable.ShapeTextView_shape_angle override val gradientTypeStyleable = R.styleable.ShapeTextView_shape_gradientType override val centerXStyleable = R.styleable.ShapeTextView_shape_centerX override val centerYStyleable = R.styleable.ShapeTextView_shape_centerY override val gradientRadiusStyleable = R.styleable.ShapeTextView_shape_gradientRadius override val strokeColorStyleable = R.styleable.ShapeTextView_shape_strokeColor override val strokePressedColorStyleable = R.styleable.ShapeTextView_shape_strokePressedColor override val strokeDisabledColorStyleable = R.styleable.ShapeTextView_shape_strokeDisabledColor override val strokeFocusedColorStyleable = R.styleable.ShapeTextView_shape_strokeFocusedColor override val strokeSelectedColorStyleable = R.styleable.ShapeTextView_shape_strokeSelectedColor override val strokeWidthStyleable = R.styleable.ShapeTextView_shape_strokeWidth override val dashWidthStyleable = R.styleable.ShapeTextView_shape_dashWidth override val dashGapStyleable = R.styleable.ShapeTextView_shape_dashGap /** * [ITextColorStyleable] */ override val textColorStyleable = R.styleable.ShapeTextView_shape_textColor override val textPressedColorStyleable = R.styleable.ShapeTextView_shape_textPressedColor override val textDisabledColorStyleable = R.styleable.ShapeTextView_shape_textDisabledColor override val textFocusedColorStyleable = R.styleable.ShapeTextView_shape_textFocusedColor override val textSelectedColorStyleable = R.styleable.ShapeTextView_shape_textSelectedColor override val textStartColorStyleable = R.styleable.ShapeTextView_shape_textStartColor override val textCenterColorStyleable = R.styleable.ShapeTextView_shape_textCenterColor override val textEndColorStyleable = R.styleable.ShapeTextView_shape_textEndColor override val textGradientOrientationStyleable = R.styleable.ShapeTextView_shape_textGradientOrientation }
扩展:因为原作者的Shape使用场景、范围较广,所以有挺多Layout-Styleable,内部实现也均有所不同,因为该篇主要记录渐变文本,所以我们仅需关注ShapeTextViewStyleable即可
Builder
关于 ShapeDrawableBuilder 和 TextColorBuilder 的封装剥离,个人认为这样的封装方式主要有以下考虑
- 单一职责,解耦(分别作用于背景和TextView自身)
- 支持自定义属性设置方式(含静态设置、动态设置)
- 建造者模式,便于链式动态设置自定义属性
- 封装一些通用型方法
ShapeDrawableBuilder
主要作用于Shape背景相关属性设置
package com.example.shapefontbg.shape.builder; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.StateListDrawable; import android.view.View; import androidx.annotation.Nullable; import com.example.shapefontbg.shape.styleable.IShapeDrawableStyleable; /** * author : Android 轮子哥 * github : https://github.com/getActivity/ShapeView * time : 2021/08/28 * desc : ShapeDrawable 构建类 */ @SuppressWarnings("unused") public final class ShapeDrawableBuilder { private static final int NO_COLOR = Color.TRANSPARENT; private final View mView; private int mShape; private int mShapeWidth; private int mShapeHeight; private int mSolidColor; private Integer mSolidPressedColor; private Integer mSolidCheckedColor; private Integer mSolidDisabledColor; private Integer mSolidFocusedColor; private Integer mSolidSelectedColor; private float mTopLeftRadius; private float mTopRightRadius; private float mBottomLeftRadius; private float mBottomRightRadius; private int[] mGradientColors; private boolean mUseLevel; private int mAngle; private int mGradientType; private float mCenterX; private float mCenterY; private int mGradientRadius; private int mStrokeColor; private Integer mStrokePressedColor; private Integer mStrokeCheckedColor; private Integer mStrokeDisabledColor; private Integer mStrokeFocusedColor; private Integer mStrokeSelectedColor; private int mStrokeWidth; private int mDashWidth; private int mDashGap; public ShapeDrawableBuilder(View view, TypedArray typedArray, IShapeDrawableStyleable styleable) { mView = view; mShape = typedArray.getInt(styleable.getShapeTypeStyleable(), 0); mShapeWidth = typedArray.getDimensionPixelSize(styleable.getShapeWidthStyleable(), -1); mShapeHeight = typedArray.getDimensionPixelSize(styleable.getShapeHeightStyleable(), -1); mSolidColor = typedArray.getColor(styleable.getSolidColorStyleable(), NO_COLOR); if (typedArray.hasValue(styleable.getSolidPressedColorStyleable())) { mSolidPressedColor = typedArray.getColor(styleable.getSolidPressedColorStyleable(), NO_COLOR); } if (styleable.getSolidCheckedColorStyleable() > 0 && typedArray.hasValue(styleable.getSolidCheckedColorStyleable())) { mSolidCheckedColor = typedArray.getColor(styleable.getSolidCheckedColorStyleable(), NO_COLOR); } if (typedArray.hasValue(styleable.getSolidDisabledColorStyleable())) { mSolidDisabledColor = typedArray.getColor(styleable.getSolidDisabledColorStyleable(), NO_COLOR); } if (typedArray.hasValue(styleable.getSolidFocusedColorStyleable())) { mSolidFocusedColor = typedArray.getColor(styleable.getSolidFocusedColorStyleable(), NO_COLOR); } if (typedArray.hasValue(styleable.getSolidSelectedColorStyleable())) { mSolidSelectedColor = typedArray.getColor(styleable.getSolidSelectedColorStyleable(), NO_COLOR); } int radius = typedArray.getDimensionPixelSize(styleable.getRadiusStyleable(), 0); mTopLeftRadius = typedArray.getDimensionPixelSize(styleable.getTopLeftRadiusStyleable(), radius); mTopRightRadius = typedArray.getDimensionPixelSize(styleable.getTopRightRadiusStyleable(), radius); mBottomLeftRadius = typedArray.getDimensionPixelSize(styleable.getBottomLeftRadiusStyleable(), radius); mBottomRightRadius = typedArray.getDimensionPixelSize(styleable.getBottomRightRadiusStyleable(), radius); if (typedArray.hasValue(styleable.getStartColorStyleable()) && typedArray.hasValue(styleable.getEndColorStyleable())) { if (typedArray.hasValue(styleable.getCenterColorStyleable())) { mGradientColors = new int[]{typedArray.getColor(styleable.getStartColorStyleable(), NO_COLOR), typedArray.getColor(styleable.getCenterColorStyleable(), NO_COLOR), typedArray.getColor(styleable.getEndColorStyleable(), NO_COLOR)}; } else { mGradientColors = new int[]{typedArray.getColor(styleable.getStartColorStyleable(), NO_COLOR), typedArray.getColor(styleable.getEndColorStyleable(), NO_COLOR)}; } } mUseLevel = typedArray.getBoolean(styleable.getUseLevelStyleable(), false); mAngle = (int) typedArray.getFloat(styleable.getAngleStyleable(), 0); mGradientType = typedArray.getInt(styleable.getGradientTypeStyleable(), GradientDrawable.LINEAR_GRADIENT); mCenterX = typedArray.getFloat(styleable.getCenterXStyleable(), 0.5f); mCenterY = typedArray.getFloat(styleable.getCenterYStyleable(), 0.5f); mGradientRadius = typedArray.getDimensionPixelSize(styleable.getGradientRadiusStyleable(), radius); mStrokeColor = typedArray.getColor(styleable.getStrokeColorStyleable(), NO_COLOR); if (typedArray.hasValue(styleable.getStrokePressedColorStyleable())) { mStrokePressedColor = typedArray.getColor(styleable.getStrokePressedColorStyleable(), NO_COLOR); } if (styleable.getStrokeCheckedColorStyleable() > 0 && typedArray.hasValue(styleable.getStrokeCheckedColorStyleable())) { mStrokeCheckedColor = typedArray.getColor(styleable.getStrokeCheckedColorStyleable(), NO_COLOR); } if (typedArray.hasValue(styleable.getStrokeDisabledColorStyleable())) { mStrokeDisabledColor = typedArray.getColor(styleable.getStrokeDisabledColorStyleable(), NO_COLOR); } if (typedArray.hasValue(styleable.getStrokeFocusedColorStyleable())) { mStrokeFocusedColor = typedArray.getColor(styleable.getStrokeFocusedColorStyleable(), NO_COLOR); } if (typedArray.hasValue(styleable.getStrokeSelectedColorStyleable())) { mStrokeSelectedColor = typedArray.getColor(styleable.getStrokeSelectedColorStyleable(), NO_COLOR); } mStrokeWidth = typedArray.getDimensionPixelSize(styleable.getStrokeWidthStyleable(), 0); mDashWidth = typedArray.getDimensionPixelSize(styleable.getDashWidthStyleable(), 0); mDashGap = typedArray.getDimensionPixelSize(styleable.getDashGapStyleable(), 0); } public ShapeDrawableBuilder setShape(int shape) { mShape = shape; return this; } public int getShape() { return mShape; } public ShapeDrawableBuilder setShapeWidth(int width) { mShapeWidth = width; return this; } public int getShapeWidth() { return mShapeWidth; } public ShapeDrawableBuilder setShapeHeight(int height) { mShapeHeight = height; return this; } public int getShapeHeight() { return mShapeHeight; } public ShapeDrawableBuilder setSolidColor(int color) { mSolidColor = color; clearGradientColors(); return this; } public int getSolidColor() { return mSolidColor; } public ShapeDrawableBuilder setSolidPressedColor(Integer color) { mSolidPressedColor = color; return this; } @Nullable public Integer getSolidPressedColor() { return mSolidPressedColor; } public ShapeDrawableBuilder setSolidCheckedColor(Integer color) { mSolidCheckedColor = color; return this; } @Nullable public Integer getSolidCheckedColor() { return mSolidCheckedColor; } public ShapeDrawableBuilder setSolidDisabledColor(Integer color) { mSolidDisabledColor = color; return this; } @Nullable public Integer getSolidDisabledColor() { return mSolidDisabledColor; } public ShapeDrawableBuilder setSolidFocusedColor(Integer color) { mSolidFocusedColor = color; return this; } @Nullable public Integer getSolidFocusedColor() { return mSolidFocusedColor; } public ShapeDrawableBuilder setSolidSelectedColor(Integer color) { mSolidSelectedColor = color; return this; } @Nullable public Integer getSolidSelectedColor() { return mSolidSelectedColor; } public ShapeDrawableBuilder setRadius(float radius) { return setRadius(radius, radius, radius, radius); } public ShapeDrawableBuilder setRadius(float topLeftRadius, float topRightRadius, float bottomLeftRadius, float bottomRightRadius) { mTopLeftRadius = topLeftRadius; mTopRightRadius = topRightRadius; mBottomLeftRadius = bottomLeftRadius; mBottomRightRadius = bottomRightRadius; return this; } public float getTopLeftRadius() { return mTopLeftRadius; } public float getTopRightRadius() { return mTopRightRadius; } public float getBottomLeftRadius() { return mBottomLeftRadius; } public float getBottomRightRadius() { return mBottomRightRadius; } public ShapeDrawableBuilder setGradientColors(int startColor, int endColor) { return setGradientColors(new int[]{startColor, endColor}); } public ShapeDrawableBuilder setGradientColors(int startColor, int centerColor, int endColor) { return setGradientColors(new int[]{startColor, centerColor, endColor}); } public ShapeDrawableBuilder setGradientColors(int[] colors) { mGradientColors = colors; return this; } @Nullable public int[] getGradientColors() { return mGradientColors; } public boolean isGradientColors() { return mGradientColors != null && mGradientColors.length > 0; } public void clearGradientColors() { mGradientColors = null; } public ShapeDrawableBuilder setUseLevel(boolean useLevel) { mUseLevel = useLevel; return this; } public boolean isUseLevel() { return mUseLevel; } public ShapeDrawableBuilder setAngle(int angle) { mAngle = angle; return this; } public int getAngle() { return mAngle; } public ShapeDrawableBuilder setGradientType(int type) { mGradientType = type; return this; } public int getGradientType() { return mGradientType; } public ShapeDrawableBuilder setCenterX(float x) { mCenterX = x; return this; } public float getCenterX() { return mCenterX; } public ShapeDrawableBuilder setCenterY(float y) { mCenterY = y; return this; } public float getCenterY() { return mCenterY; } public ShapeDrawableBuilder setGradientRadius(int radius) { mGradientRadius = radius; return this; } public int getGradientRadius() { return mGradientRadius; } public ShapeDrawableBuilder setStrokeColor(int color) { mStrokeColor = color; return this; } public int getStrokeColor() { return mStrokeColor; } public ShapeDrawableBuilder setStrokePressedColor(Integer color) { mStrokePressedColor = color; return this; } @Nullable public Integer getStrokePressedColor() { return mStrokePressedColor; } public ShapeDrawableBuilder setStrokeCheckedColor(Integer color) { mStrokeCheckedColor = color; return this; } @Nullable public Integer getStrokeCheckedColor() { return mStrokeCheckedColor; } public ShapeDrawableBuilder setStrokeDisabledColor(Integer color) { mStrokeDisabledColor = color; return this; } @Nullable public Integer getStrokeDisabledColor() { return mStrokeDisabledColor; } public ShapeDrawableBuilder setStrokeFocusedColor(Integer color) { mStrokeFocusedColor = color; return this; } @Nullable public Integer getStrokeFocusedColor() { return mStrokeFocusedColor; } public ShapeDrawableBuilder setStrokeSelectedColor(Integer color) { mStrokeSelectedColor = color; return this; } @Nullable public Integer getStrokeSelectedColor() { return mStrokeSelectedColor; } public ShapeDrawableBuilder setStrokeWidth(int width) { mStrokeWidth = width; return this; } public int getStrokeWidth() { return mStrokeWidth; } public ShapeDrawableBuilder setDashWidth(int width) { mDashWidth = width; return this; } public int getDashWidth() { return mDashWidth; } public ShapeDrawableBuilder setDashGap(int gap) { mDashGap = gap; return this; } public int getDashGap() { return mDashGap; } public boolean isDashLineEnable() { return mDashGap > 0; } public Drawable buildBackgroundDrawable() { if (!isGradientColors() && mSolidColor == NO_COLOR && mStrokeColor == NO_COLOR) { return null; } GradientDrawable defaultDrawable = createGradientDrawable(mSolidColor, mStrokeColor); // 判断是否设置了渐变色 if (isGradientColors()) { defaultDrawable.setColors(mGradientColors); } if (mSolidPressedColor != null && mStrokePressedColor != null && mSolidCheckedColor != null && mStrokeCheckedColor != null && mSolidDisabledColor != null && mStrokeDisabledColor != null && mSolidFocusedColor != null && mStrokeFocusedColor != null && mSolidSelectedColor != null && mStrokeSelectedColor != null) { return defaultDrawable; } StateListDrawable drawable = new StateListDrawable(); if (mSolidPressedColor != null || mStrokePressedColor != null) { drawable.addState(new int[]{android.R.attr.state_pressed}, createGradientDrawable( mSolidPressedColor != null ? mSolidPressedColor : mSolidColor, mStrokePressedColor != null ? mStrokePressedColor : mStrokeColor)); } if (mSolidCheckedColor != null || mStrokeCheckedColor != null) { drawable.addState(new int[]{android.R.attr.state_checked}, createGradientDrawable( mSolidCheckedColor != null ? mSolidCheckedColor : mSolidColor, mStrokeCheckedColor != null ? mStrokeCheckedColor : mStrokeColor)); } if (mSolidDisabledColor != null || mStrokeDisabledColor != null) { drawable.addState(new int[]{-android.R.attr.state_enabled}, createGradientDrawable( mSolidDisabledColor != null ? mSolidDisabledColor : mSolidColor, mStrokeDisabledColor != null ? mStrokeDisabledColor : mStrokeColor)); } if (mSolidFocusedColor != null || mStrokeFocusedColor != null) { drawable.addState(new int[]{android.R.attr.state_focused}, createGradientDrawable( mSolidFocusedColor != null ? mSolidFocusedColor : mSolidColor, mStrokeFocusedColor != null ? mStrokeFocusedColor : mStrokeColor)); } if (mSolidSelectedColor != null || mStrokeSelectedColor != null) { drawable.addState(new int[]{android.R.attr.state_selected}, createGradientDrawable( mSolidSelectedColor != null ? mSolidSelectedColor : mSolidColor, mStrokeSelectedColor != null ? mStrokeSelectedColor : mStrokeColor)); } drawable.addState(new int[]{}, defaultDrawable); return drawable; } public void intoBackground() { Drawable drawable = buildBackgroundDrawable(); if (drawable == null) { return; } // if (isDashLineEnable() || isShadowEnable()) { // // 需要关闭硬件加速,否则虚线或者阴影在某些手机上面无法生效 // mView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); // } mView.setBackground(drawable); } private GradientDrawable createGradientDrawable(int solidColor, int strokeColor) { //top-left, top-right, bottom-right, bottom-left. float[] radius = {mTopLeftRadius, mTopLeftRadius, mTopRightRadius, mTopRightRadius, mBottomRightRadius, mBottomRightRadius, mBottomLeftRadius, mBottomLeftRadius}; GradientDrawable gradientDrawable = new GradientDrawable(); gradientDrawable.setShape(mShape); // 形状 gradientDrawable.setSize(mShapeWidth, mShapeHeight); // 尺寸 gradientDrawable.setCornerRadii(radius); // 圆角 gradientDrawable.setColor(solidColor); // 颜色 gradientDrawable.setUseLevel(mUseLevel); gradientDrawable.setStroke(strokeColor, mStrokeWidth, mDashWidth, mDashGap); // 边框 gradientDrawable.setOrientation(toOrientation(mAngle)); gradientDrawable.setGradientType(mGradientType); gradientDrawable.setGradientRadius(mGradientRadius); gradientDrawable.setGradientCenter(mCenterX, mCenterY); return gradientDrawable; } public GradientDrawable.Orientation toOrientation(int angle) { angle %= 360; // angle 必须为 45 的整数倍 if (angle % 45 == 0) { switch (angle) { case 0: return GradientDrawable.Orientation.LEFT_RIGHT; case 45: return GradientDrawable.Orientation.BL_TR; case 90: return GradientDrawable.Orientation.BOTTOM_TOP; case 135: return GradientDrawable.Orientation.BR_TL; case 180: return GradientDrawable.Orientation.RIGHT_LEFT; case 225: return GradientDrawable.Orientation.TR_BL; case 270: return GradientDrawable.Orientation.TOP_BOTTOM; case 315: return GradientDrawable.Orientation.TL_BR; default: break; } } return GradientDrawable.Orientation.LEFT_RIGHT; } }
设置shape背景场景,可以 一 一对应属性
场景判断
背景属性具体设置方式,包含角度处理
TextColorBuilder
主要作用于 TextView本身属性设置
package com.example.shapefontbg.shape.builder; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.text.SpannableStringBuilder; import android.widget.TextView; import androidx.annotation.Nullable; import com.example.shapefontbg.shape.LinearGradientFontSpan; import com.example.shapefontbg.shape.styleable.ITextColorStyleable; /** * author : Android 轮子哥 * github : https://github.com/getActivity/ShapeView * time : 2021/08/28 * desc : TextColor 构建类 */ @SuppressWarnings("unused") public final class TextColorBuilder { private final TextView mTextView; private int mTextColor; private Integer mTextPressedColor; private Integer mTextCheckedColor; private Integer mTextDisabledColor; private Integer mTextFocusedColor; private Integer mTextSelectedColor; private int[] mTextGradientColors; private int mTextGradientOrientation; public TextColorBuilder(TextView textView, TypedArray typedArray, ITextColorStyleable styleable) { mTextView = textView; mTextColor = typedArray.getColor(styleable.getTextColorStyleable(), textView.getTextColors().getDefaultColor()); if (typedArray.hasValue(styleable.getTextPressedColorStyleable())) { mTextPressedColor = typedArray.getColor(styleable.getTextPressedColorStyleable(), mTextColor); } if (styleable.getTextCheckedColorStyleable() > 0 && typedArray.hasValue(styleable.getTextCheckedColorStyleable())) { mTextCheckedColor = typedArray.getColor(styleable.getTextCheckedColorStyleable(), mTextColor); } if (typedArray.hasValue(styleable.getTextDisabledColorStyleable())) { mTextDisabledColor = typedArray.getColor(styleable.getTextDisabledColorStyleable(), mTextColor); } if (typedArray.hasValue(styleable.getTextFocusedColorStyleable())) { mTextFocusedColor = typedArray.getColor(styleable.getTextFocusedColorStyleable(), mTextColor); } if (typedArray.hasValue(styleable.getTextSelectedColorStyleable())) { mTextSelectedColor = typedArray.getColor(styleable.getTextSelectedColorStyleable(), mTextColor); } if (typedArray.hasValue(styleable.getTextStartColorStyleable()) && typedArray.hasValue(styleable.getTextEndColorStyleable())) { if (typedArray.hasValue(styleable.getTextCenterColorStyleable())) { mTextGradientColors = new int[]{typedArray.getColor(styleable.getTextStartColorStyleable(), mTextColor), typedArray.getColor(styleable.getTextCenterColorStyleable(), mTextColor), typedArray.getColor(styleable.getTextEndColorStyleable(), mTextColor)}; } else { mTextGradientColors = new int[]{typedArray.getColor(styleable.getTextStartColorStyleable(), mTextColor), typedArray.getColor(styleable.getTextEndColorStyleable(), mTextColor)}; } } mTextGradientOrientation = typedArray.getColor(styleable.getTextGradientOrientationStyleable(), LinearGradientFontSpan.GRADIENT_ORIENTATION_HORIZONTAL); } public TextColorBuilder setTextColor(int color) { mTextColor = color; clearTextGradientColors(); return this; } public int getTextColor() { return mTextColor; } public TextColorBuilder setTextPressedColor(Integer color) { mTextPressedColor = color; return this; } @Nullable public Integer getTextPressedColor() { return mTextPressedColor; } public TextColorBuilder setTextCheckedColor(Integer color) { mTextCheckedColor = color; return this; } @Nullable public Integer getTextCheckedColor() { return mTextCheckedColor; } public TextColorBuilder setTextDisabledColor(Integer color) { mTextDisabledColor = color; return this; } @Nullable public Integer getTextDisabledColor() { return mTextDisabledColor; } public TextColorBuilder setTextFocusedColor(Integer color) { mTextFocusedColor = color; return this; } @Nullable public Integer getTextFocusedColor() { return mTextFocusedColor; } public TextColorBuilder setTextSelectedColor(Integer color) { mTextSelectedColor = color; return this; } @Nullable public Integer getTextSelectedColor() { return mTextSelectedColor; } public TextColorBuilder setTextGradientColors(int startColor, int endColor) { return setTextGradientColors(new int[]{startColor, endColor}); } public TextColorBuilder setTextGradientColors(int startColor, int centerColor, int endColor) { return setTextGradientColors(new int[]{startColor, centerColor, endColor}); } public TextColorBuilder setTextGradientColors(int[] colors) { mTextGradientColors = colors; return this; } @Nullable public int[] getTextGradientColors() { return mTextGradientColors; } public boolean isTextGradientColors() { return mTextGradientColors != null && mTextGradientColors.length > 0; } public void clearTextGradientColors() { mTextGradientColors = null; } public TextColorBuilder setTextGradientOrientation(int orientation) { mTextGradientOrientation = orientation; return this; } public int getTextGradientOrientation() { return mTextGradientOrientation; } public SpannableStringBuilder buildLinearGradientSpannable(CharSequence text) { return LinearGradientFontSpan.buildLinearGradientSpannable(text, mTextGradientColors, null, mTextGradientOrientation); } public ColorStateList buildColorState() { if (mTextPressedColor == null && mTextCheckedColor == null && mTextDisabledColor == null && mTextFocusedColor == null && mTextSelectedColor == null) { return ColorStateList.valueOf(mTextColor); } int maxSize = 6; int arraySize = 0; int[][] statesTemp = new int[maxSize][]; int[] colorsTemp = new int[maxSize]; if (mTextPressedColor != null) { statesTemp[arraySize] = new int[]{android.R.attr.state_pressed}; colorsTemp[arraySize] = mTextPressedColor; arraySize++; } if (mTextCheckedColor != null) { statesTemp[arraySize] = new int[]{android.R.attr.state_checked}; colorsTemp[arraySize] = mTextCheckedColor; arraySize++; } if (mTextDisabledColor != null) { statesTemp[arraySize] = new int[]{-android.R.attr.state_enabled}; colorsTemp[arraySize] = mTextDisabledColor; arraySize++; } if (mTextFocusedColor != null) { statesTemp[arraySize] = new int[]{android.R.attr.state_focused}; colorsTemp[arraySize] = mTextFocusedColor; arraySize++; } if (mTextSelectedColor != null) { statesTemp[arraySize] = new int[]{android.R.attr.state_selected}; colorsTemp[arraySize] = mTextSelectedColor; arraySize++; } statesTemp[arraySize] = new int[]{}; colorsTemp[arraySize] = mTextColor; arraySize++; int[][] states; int[] colors; if (arraySize == maxSize) { states = statesTemp; colors = colorsTemp; } else { states = new int[arraySize][]; colors = new int[arraySize]; // 对数组进行拷贝 System.arraycopy(statesTemp, 0, states, 0, arraySize); System.arraycopy(colorsTemp, 0, colors, 0, arraySize); } return new ColorStateList(states, colors); } public void intoTextColor() { if (isTextGradientColors()) { mTextView.setText(buildLinearGradientSpannable(mTextView.getText())); return; } mTextView.setTextColor(buildColorState()); } }
LinearGradientFontSpan 文本渐变核心类
通过 LinearGradient 设置渐变效果
package com.example.shapefontbg.shape; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Shader; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.ReplacementSpan; import android.widget.LinearLayout; import androidx.annotation.NonNull; /** * author : Android 轮子哥 * github : https://github.com/getActivity/ShapeView * time : 2021/08/17 * desc : 支持直接定义文本渐变色的 Span */ public class LinearGradientFontSpan extends ReplacementSpan { /** * 水平渐变方向 */ public static final int GRADIENT_ORIENTATION_HORIZONTAL = LinearLayout.HORIZONTAL; /** * 垂直渐变方向 */ public static final int GRADIENT_ORIENTATION_VERTICAL = LinearLayout.VERTICAL; /** * 构建一个文字渐变色的 Spannable 对象 */ public static SpannableStringBuilder buildLinearGradientSpannable(CharSequence text, int[] colors, float[] positions, int orientation) { SpannableStringBuilder builder = new SpannableStringBuilder(text); //下面声明了建造方法,所以支持链式设置 LinearGradientFontSpan span = new LinearGradientFontSpan() .setTextGradientColor(colors) .setTextGradientOrientation(orientation) .setTextGradientPositions(positions); builder.setSpan(span, 0, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return builder; } /** * 测量的文本宽度 */ private float mMeasureTextWidth; /** * 文字渐变方向 */ private int mTextGradientOrientation; /** * 文字渐变颜色组 */ private int[] mTextGradientColor; /** * 文字渐变位置组 */ private float[] mTextGradientPositions; @Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fontMetricsInt) { mMeasureTextWidth = paint.measureText(text, start, end); // 这段不可以去掉,字体高度没设置,会出现 draw 方法没有被调用的问题 // 详情请见:https://stackoverflow.com/questions/20069537/replacementspans-draw-method-isnt-called Paint.FontMetricsInt metrics = paint.getFontMetricsInt(); if (fontMetricsInt != null) { fontMetricsInt.top = metrics.top; fontMetricsInt.ascent = metrics.ascent; fontMetricsInt.descent = metrics.descent; fontMetricsInt.bottom = metrics.bottom; } return (int) mMeasureTextWidth; } @Override public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { LinearGradient linearGradient; if (mTextGradientOrientation == GRADIENT_ORIENTATION_VERTICAL) { linearGradient = new LinearGradient(0, 0, 0, paint.descent() - paint.ascent(), mTextGradientColor, mTextGradientPositions, Shader.TileMode.REPEAT); } else { linearGradient = new LinearGradient(x, 0, x + mMeasureTextWidth, 0, mTextGradientColor, mTextGradientPositions, Shader.TileMode.REPEAT); } paint.setShader(linearGradient); int alpha = paint.getAlpha(); // 判断是否给画笔设置了透明度 if (alpha != 255) { // 如果是则设置不透明 paint.setAlpha(255); } canvas.drawText(text, start, end, x, y, paint); // 绘制完成之后将画笔的透明度还原回去 paint.setAlpha(alpha); } public LinearGradientFontSpan setTextGradientOrientation(int orientation) { mTextGradientOrientation = orientation; return this; } public LinearGradientFontSpan setTextGradientColor(int[] colors) { mTextGradientColor = colors; return this; } public LinearGradientFontSpan setTextGradientPositions(float[] positions) { mTextGradientPositions = positions; return this; } }
测量渐变文本的宽度
渐变方向、渐变颜色、画笔透明度处理等~
还没有评论,来说两句吧...