android:foreground attribute / setForeground() met

2019-06-02 09:56发布

问题:

Since Android 23, the android:foreground XML attribute (and corresponding setForeground() method) should be available to all Views, and not just FrameLayout instances as was previously the case.

Yet, for some reason, whenever I create a Button instance which inherits from TextView -> View, I can't seem to get a foreground to show at all.

Here is an example button definition that doesn't work appear to show the foreground:

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

And here is the defintion of the Button.Primary style:

    <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>

I've confirmed that setting the foreground attribute in the style or in the <Button> definition does not change anything, the foreground fails to show regardless. The background shows correctly, as does the text, it's just the foreground that's missing.

I've also tried setting the foreground to a solid color instead of a state selector drawable, with no success.

回答1:

After removing the style elements one by one and retesting, I narrowed down the issue to the inclusion of the android:singleLine attribute in the style.

Removing this attribute made it so any TextViews or Buttons using my style would properly show the foreground drawable as desired. Looking through the implementation of TextView.java where singleLine is defined, I'm still struggling to determine how setting this attribute causes foregrounds to be ignored, but I will update this answer if I find out.

I know that use of singleLine is deprecated but unfortunately I still need the functionality provided by the combination of the singleLine / ellipsize attributes which are not available when replacing with maxLines usage.

Edit: After writing out my answer above, I decided to do some more investigation and uncovered a few more details.

First, I created a custom view that extended from AppCompatButton so I could attempt to re-implement the functionality singleLine / ellipsize provided (namely, showing text on a single line by replacing newline characters with spaces and then also adding ellipsis if the text ran off the view).

Reading through the TextView source code, I found a section of code that is called when the singleLine attribute is set to true:

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

So I tried extracting those lines and adding to my own implementation:

    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);
    }
}

But when this code ran, I saw the same issue from before where foreground drawables would no longer be visible.

After further testing, I was able to narrow down the issue to the setHorizontallyScrolling(true) method call. If I commented just this line out of my custom implementation, I'm able to preserve the singleLine / ellipsize functionality I had before and foreground drawables show as expected! Narrowing it down to that method still didn't help me figure out the root cause in the TextView base implementation, but if anyone has any details on that, feel free to comment with more information.

Here is my final custom Button class which simply looks at the maxLines attribute and simulates the old singleLine functionality when maxLines is set to 1, avoiding the method call that would prevent foregrounds from showing. In case it's useful...

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);
    }
}

}

and from attrs.xml:

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