android:foreground 属性/setForeground() 方法不适用于 Button 元素

android:foreground attribute / setForeground() method does not work with Button elements

因为 Android 23, the android:foreground XML attribute (and corresponding setForeground() method) 应该对所有视图可用,而不是像以前那样只对 FrameLayout 实例可用。

然而,出于某种原因,每当我创建一个继承自 TextView -> View 的 Button 实例时,我似乎根本无法显示前景。

这是一个示例按钮定义,它似乎无法显示前景:

    <Button
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_marginEnd="10dp"
      android:text="Primary Full Width"
      style="@style/Button.Primary" />

下面是 Button.Primary 样式的定义:

    <style name="Button.Primary" parent="Widget.AppCompat.Button">
      <item name="android:background">@drawable/button_primary_background_selector</item>
      <item name="android:foreground">@drawable/button_primary_foreground_selector</item>
      <item name="android:textAppearance">@style/TextAppearance.FF.Button</item>
      <item name="android:minHeight">80dp</item>
      <item name="android:height">80dp</item>
      <item name="android:minWidth">200dp</item>
      <item name="android:defaultWidth">288dp</item>
      <item name="android:maxWidth">400dp</item>
      <item name="android:focusable">true</item>
      <item name="android:clickable">true</item>
      <item name="android:gravity">center_vertical|center_horizontal</item>
      <item name="android:paddingStart">70dp</item>
      <item name="android:paddingEnd">70dp</item>
      <item name="android:stateListAnimator">@null</item>
      <item name="android:singleLine">true</item>
      <item name="android:ellipsize">end</item>
      <item name="android:drawablePadding">10dp</item>
</style>

我已经确认在样式或 <Button> 定义中设置 foreground 属性不会改变任何东西,无论如何前景都无法显示。背景和文字都正确显示,只是缺少前景。

我也试过将前景设置为纯色而不是状态选择器可绘制,但没有成功。

在将样式元素一一删除并重新测试后,我将问题缩小到样式中包含 android:singleLine 属性。

删除此属性后,任何使用我的样式的 TextView 或按钮都可以根据需要正确显示前景可绘制对象。查看定义了 singleLineTextView.java 的实现,我仍在努力确定设置此属性如何导致前景被忽略,但如果我发现,我会更新此答案。

我知道 singleLine 的使用已被弃用,但不幸的是,我仍然需要 singleLine / ellipsize 属性组合提供的功能,这些属性在替换为 [ 时不可用=19=]用法。

编辑: 在写完上面的答案后,我决定做更多的调查并发现更多的细节。

首先,我创建了一个从 AppCompatButton 扩展的自定义视图,这样我就可以尝试重新实现 singleLine / ellipsize 提供的功能(即,通过替换换行符在单行上显示文本带空格的字符,如果文本 运行 不在视图中,则还要添加省略号)。

通读 TextView source code,我发现了一段代码,当 singleLine 属性设置为 true 时调用:

private void applySingleLine(boolean singleLine, boolean applyTransformation,
        boolean changeMaxLines) {
    mSingleLine = singleLine;
    if (singleLine) {
        setLines(1);
        setHorizontallyScrolling(true);
        if (applyTransformation) {
            setTransformationMethod(SingleLineTransformationMethod.getInstance());
        }
    } 
    ...
}

所以我尝试提取这些行并添加到我自己的实现中:

    private void updateMaxlinesLocally(int maxLines) {
    if (maxLines == 1) {
        Log.d("Button", "max lines was 1, setting to single line equivalent");
        setLines(1);
        setHorizontallyScrolling(true);

 setTransformationMethod(SingleLineTransformationMethod.getInstance());
        // reset any text that may have already been set
        setText(getText());
    } else {
        Log.d("Button", "max lines was  : " + maxLines);
    }
}

但是当这段代码 运行 时,我看到了与之前相同的问题,前景可绘制对象将不再可见。

经过进一步测试,我能够将问题缩小到 setHorizontallyScrolling(true) 方法调用。如果我在我的自定义实现中只评论了这一行,我就能够保留我之前拥有的 singleLine / ellipsize 功能,并且前景可绘制对象会按预期显示!将其缩小到该方法仍然无法帮助我找出 TextView 基本实现中的根本原因,但如果有人对此有任何详细信息,请随时发表评论并提供更多信息。

这是我的最终自定义按钮 class,它只查看 maxLines 属性并在 maxLines 设置为 1 时模拟旧的 singleLine 功能,避免阻止前景显示的方法调用。万一有用...

public class Button extends AppCompatButton {

public Button(Context context) {
    super(context);
    init(context, null, 0);
}

public Button(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context, attrs, 0);
}

public Button(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context, attrs, defStyleAttr);
}

@Override
public void setMaxLines(int maxLines) {
    super.setMaxLines(maxLines);
    updateMaxlinesLocally(maxLines);
}

private void init(Context context, AttributeSet attributeSet, int defStyleAttr) {
    int[] set = {
            android.R.attr.maxLines
    };
    TypedArray a = context.obtainStyledAttributes(
            attributeSet, R.styleable.Button);
    int maxLines = a.getInt(R.styleable.Button_android_maxLines, -1);
    a.recycle();
    updateMaxlinesLocally(maxLines);
}

private void updateMaxlinesLocally(int maxLines) {
    if (maxLines == 1) {
        Log.d("Button", "max lines was 1, setting to single line equivalent");
        setLines(1);
        // The act of setting horizontally scrolling to true is what disables foregrounds from
        // showing...
//            setHorizontallyScrolling(true);
        setTransformationMethod(SingleLineTransformationMethod.getInstance());
        // reset any text that may have already been set
        setText(getText());
    } else {
        Log.d("Button", "max lines was  : " + maxLines);
    }
}

}

来自attrs.xml:

<declare-styleable name="Button">
    <attr name="android:maxLines" format="integer" />
</declare-styleable>

正如 Keith setHorizontallyScrolling(true) 所述,滚动会使前景消失。很容易重现问题。

但是为什么横向滚动会影响前景呢?

设置水平(或垂直滚动​​)告诉 TextView 它包含比布局边界更大的区域。

如果您打印 scrollX / scrollY 值,通常它不会是零,而是一个非常大的数字,即使用户无法滚动内容也是如此。

另一方面,前景设计为随内容滚动,所以它基本上是从 canvas 的 (0, 0) 开始绘制的,而不是可见内容的左上角。所以它绘制得离实际视图边界很远。你可以打开View.java获取onDrawForeground的源代码。

我能做什么

  • 在设置 singleline 属性或任何暗示滚动的内容后调用 setHorizontallyScrolling(false) 来禁用滚动。当心不要在尝试初始化/更新视图时启用滚动。
  • 翻译前景可绘制对象,例如通过添加 scrollX & scrollY
  • setBounds 更新边界