Android 中带有渐变的文本

我该如何扩展 TextView来允许带有渐变效果的文本绘制?

62273 次浏览

一个简单但有些局限的解决方案是使用这些属性:

android:fadingEdge="horizontal"
android:scrollHorizontally="true"

我已经在文本字段中使用它,如果它们太长,我希望它们能够淡出。

似乎不可能扩展 TextView 来绘制带渐变的文本。但是,可以通过创建一个画布并在其上绘制来实现这种效果。首先我们需要 声明我们的自定义 UI 元素。在起始阶段,我们需要创建一个 布局的子类。在这种情况下,我们将使用 无聊的布局,它只支持单行文本。

Shader textShader=new LinearGradient(0, 0, 0, 20,
new int[]{bottom,top},
new float[]{0, 1}, TileMode.CLAMP);//Assumes bottom and top are colors defined above
textPaint.setTextSize(textSize);
textPaint.setShader(textShader);
BoringLayout.Metrics boringMetrics=BoringLayout.isBoring(text, textPaint);
boringLayout=new BoringLayout(text, textPaint, 0, Layout.Alignment.ALIGN_CENTER,
0.0f, 0.0f, boringMetrics, false);

然后我们覆盖 onMeasureonDraw:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
setMeasuredDimension((int) textPaint.measureText(text), (int) textPaint.getFontSpacing());
}


@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
boringLayout.draw(canvas);
}

此时,我们的 onDraw实现相当懒惰(它完全忽略了度量规范!,但是只要您保证给视图提供足够的空间,它就应该可以正常工作。

或者,也可以从 Canvas继承并重写 onPaint方法。如果这样做,那么不幸的是,正在绘制的文本的锚总是在底部,所以我们必须将 -textPaint.getFontMetricsInt().ascent()添加到 y 坐标。

TextView secondTextView = new TextView(this);
Shader textShader=new LinearGradient(0, 0, 0, 20,
new int[]{Color.GREEN,Color.BLUE},
new float[]{0, 1}, TileMode.CLAMP);
secondTextView.getPaint().setShader(textShader);

我已经建立了一个包含这两种方法的库。您可以以 XML 格式创建 GradientTextView,也可以只使用 GradientTextView.setGradient (TextView textView...)在常规 TextView 对象上创建 GradientTextView。

https://github.com/koush/Widgets

这里是一个线性布局的例子,你可以使用这个例子的文本视图也,在源代码中不会有梯度编码,你得到的源代码和添加的代码从该网站本身-http://android-codes-examples.blogspot.com/2011/07/design-linearlayout-or-textview-and-any.html

这里有一个不错的方法:

/**
* sets a vertical gradient on the textView's paint, so that on its onDraw method, it will use it.
*
* @param viewAlreadyHasSize
*            set to true only if the textView already has a size
*/
public static void setVerticalGradientOnTextView(final TextView tv, final int positionsAndColorsResId,
final boolean viewAlreadyHasSize) {
final String[] positionsAndColors = tv.getContext().getResources().getStringArray(positionsAndColorsResId);
final int[] colors = new int[positionsAndColors.length];
float[] positions = new float[positionsAndColors.length];
for (int i = 0; i < positionsAndColors.length; ++i) {
final String positionAndColors = positionsAndColors[i];
final int delimeterPos = positionAndColors.lastIndexOf(':');
if (delimeterPos == -1 || positions == null) {
positions = null;
colors[i] = Color.parseColor(positionAndColors);
} else {
positions[i] = Float.parseFloat(positionAndColors.substring(0, delimeterPos));
String colorStr = positionAndColors.substring(delimeterPos + 1);
if (colorStr.startsWith("0x"))
colorStr = '#' + colorStr.substring(2);
else if (!colorStr.startsWith("#"))
colorStr = '#' + colorStr;
colors[i] = Color.parseColor(colorStr);
}
}
setVerticalGradientOnTextView(tv, colors, positions, viewAlreadyHasSize);
}


/**
* sets a vertical gradient on the textView's paint, so that on its onDraw method, it will use it. <br/>
*
* @param colors
*            the colors to use. at least one should exist.
* @param tv
*            the textView to set the gradient on it
* @param positions
*            where to put each color (fraction, max is 1). if null, colors are spread evenly .
* @param viewAlreadyHasSize
*            set to true only if the textView already has a size
*/
public static void setVerticalGradientOnTextView(final TextView tv, final int[] colors, final float[] positions,
final boolean viewAlreadyHasSize) {
final Runnable runnable = new Runnable() {


@Override
public void run() {
final TileMode tile_mode = TileMode.CLAMP;
final int height = tv.getHeight();
final LinearGradient lin_grad = new LinearGradient(0, 0, 0, height, colors, positions, tile_mode);
final Shader shader_gradient = lin_grad;
tv.getPaint().setShader(shader_gradient);
}
};
if (viewAlreadyHasSize)
runnable.run();
else
runJustBeforeBeingDrawn(tv, runnable);
}


public static void runJustBeforeBeingDrawn(final View view, final Runnable runnable) {
final OnPreDrawListener preDrawListener = new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
view.getViewTreeObserver().removeOnPreDrawListener(this);
runnable.run();
return true;
}
};
view.getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}

此外,如果你想使用渐变的位图,而不是一个真正的,使用:

/**
* sets an image for the textView <br/>
* NOTE: this function must be called after you have the view have its height figured out <br/>
*/
public static void setBitmapOnTextView(final TextView tv, final Bitmap bitmap) {
final TileMode tile_mode = TileMode.CLAMP;
final int height = tv.getHeight();
final int width = tv.getWidth();
final Bitmap temp = Bitmap.createScaledBitmap(bitmap, width, height, true);
final BitmapShader bitmapShader = new BitmapShader(temp, tile_mode, tile_mode);
tv.getPaint().setShader(bitmapShader);
}

EDIT: Alternative to runJustBeforeBeingDrawn: https://stackoverflow.com/a/28136027/878126

这里是多行支持作为一个班轮。这应该工作的按钮也。

Shader shader = new LinearGradient(0,0,0,textView.getLineHeight(),
startColor, endColor, Shader.TileMode.REPEAT);
textView.getPaint().setShader(shader);

我使用了顶部的答案(@Taras)和5种颜色的渐变,但是有一个问题: textView 看起来像是我在它上面放了一个白色的封面。这是我的代码和截图。

        textView = (TextView) findViewById(R.id.main_tv);
textView.setText("Tianjin, China".toUpperCase());


TextPaint paint = textView.getPaint();
float width = paint.measureText("Tianjin, China");


Shader textShader = new LinearGradient(0, 0, width, textView.getTextSize(),
new int[]{
Color.parseColor("#F97C3C"),
Color.parseColor("#FDB54E"),
Color.parseColor("#64B678"),
Color.parseColor("#478AEA"),
Color.parseColor("#8446CC"),
}, null, Shader.TileMode.CLAMP);
textView.getPaint().setShader(textShader);

enter image description here

几个小时后,我发现我需要用渐变的第一个颜色调用 textView.setTextColor()。然后是截图:

enter image description here

希望能帮助别人!

下面是我的解决方法。使用文本跨度实现。 截图

class LinearGradientForegroundSpan extends CharacterStyle implements UpdateAppearance {
private int startColor;
private int endColor;
private int lineHeight;


public LinearGradientForegroundSpan(int startColor, int endColor, int lineHeight) {
this.startColor = startColor;
this.endColor = endColor;
this.lineHeight = lineHeight;
}


@Override
public void updateDrawState(TextPaint tp) {
tp.setShader(new LinearGradient(0, 0, 0, lineHeight,
startColor, endColor, Shader.TileMode.REPEAT));
}
}

设置渐变文本的样式。

    SpannableString gradientText = new SpannableString("Gradient Text");
gradientText.setSpan(new LinearGradientForegroundSpan(Color.RED, Color.LTGRAY, textView.getLineHeight()),
0, gradientText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableStringBuilder sb = new SpannableStringBuilder();
sb.append(gradientText);
sb.append(" Normal Text");
textView.setText(sb);

我已经找到了不使用 TextView 类扩展的方法。

class MainActivity : AppCompatActivity() {
private val textGradientOnGlobalLayoutListener = object: ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
textGradient.paint.shader = LinearGradient(0f, 0f,
textGradient.width.toFloat(),
textGradient.height.toFloat(),
color0, color1, Shader.TileMode.CLAMP)
textGradient.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
private val textGradient by lazy {
findViewById<TextView>(R.id.text_gradient)
}
private val color0 by lazy {
ContextCompat.getColor(applicationContext, R.color.purple_200)
}
private val color1 by lazy {
ContextCompat.getColor(applicationContext, R.color.teal_200)
}


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textGradient.viewTreeObserver.addOnGlobalLayoutListener(textGradientOnGlobalLayoutListener)
}
}

我已经组合了这个线程的答案并创建了一个轻量级库。您可以在 gradle 实现中使用它,也可以通过将它添加到源代码中来简单地使用所需的文件。

Https://github.com/veeyaarvr/supergradienttextview

对我有效的解决方案是在应用任何着色器之前应用文本颜色。正如问题的作者所说:

几个小时后,我发现需要使用渐变的第一个颜色调用 textView.setTextColor ()。然后是截图:

例如,最有效的方法是首先将白色设置为文本颜色。然后我们可以应用着色器,它将被应用在白色的顶部,所以我们将得到所需的渐变颜色。

Kotlin + coroutines version.

设置垂直梯度的扩展:

private fun TextView.setGradientTextColor(vararg colorRes: Int) {
val floatArray = ArrayList<Float>(colorRes.size)
for (i in colorRes.indices) {
floatArray.add(i, i.toFloat() / (colorRes.size - 1))
}
val textShader: Shader = LinearGradient(
0f,
0f,
0f,
this.height.toFloat(),
colorRes.map { ContextCompat.getColor(requireContext(), it) }.toIntArray(),
floatArray.toFloatArray(),
TileMode.CLAMP
)
this.paint.shader = textShader
}

挂起扩展。您需要等待视图更改其高度。

suspend fun View.awaitLayoutChange() = suspendCancellableCoroutine<Unit> { cont ->
val listener = object : View.OnLayoutChangeListener {
override fun onLayoutChange(
view: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
view?.removeOnLayoutChangeListener(this)
cont.resumeWith(Result.success(Unit))
}
}


addOnLayoutChangeListener(listener)
cont.invokeOnCancellation { removeOnLayoutChangeListener(listener) }

}

使用方法:

lifecycle.coroutineScope.launch {
binding.tvAmount.text = "Dumb text"
binding.tvAmount.awaitLayoutChange()
binding.tvAmount.setGradientTextColor(
R.color.yellow,
R.color.green
)
}

对科特林来说:

val paint: TextPaint = textView.paint
val width: Float = paint.measureText(holder.langs.text.toString())


val textShader: Shader = LinearGradient(0f, 0f, width, holder.langs.textSize, intArrayOf(
Color.parseColor("#8913FC"),
Color.parseColor("#00BFFC")), null, Shader.TileMode.CLAMP)
holder.langs.paint.shader = textShader