Extending Preference classes in Android Lollipop =

2019-01-26 09:47发布

Just for extending CheckBoxPreference or SwitchPreference on Android Lollipop, the widget (the checkbox or the switch) won't have animation anymore.

I'd like to extend SwitchPreference to force api < 21 to use SwitchCompat instead of the default one they are using (which is obviously wrong).

I am using the new AppCompatPreferenceActivity with appcompat-v7:22.1.1 but that doesn't seem to affect the switches.

The thing is that with just extending those classes, without adding any custom layout or widget resource layout, the animation is gone.

I know I can write two instances of my preference.xml (on inside values-v21) and it will work... But I'd like to know why is this happening and if somebody knows a solution without having two preference.xml.

Code example:

public class SwitchPreference extends android.preference.SwitchPreference {

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

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public SwitchPreference(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SwitchPreference(Context context) {
        super(context);
    }
}

This or the same for CheckBoxPreference and then using:

<com.my.package.SwitchPreference />

Will make the animation in a Lollipop device to be gone.

--

Another thing I tried for the SwitchPreference (that I can with CheckBoxPreference) is to give a layout with the default id but @android:id/switchWidgetis not public while @android:id/checkbox is. I also know I can use a <CheckBoxPreference /> and give a widget layout that is in fact a SwitchCompat, but I'd like to avoid that (confusing the names).

4条回答
爷、活的狠高调
2楼-- · 2019-01-26 10:32

It seems I found a fix for your issue.

Extensive Explanation

In SwitchCompat, when toggling the the switch, it tests a few functions before playing the animation: getWindowToken() != null && ViewCompat.isLaidOut(this) && isShown().

Full method:

@Override
public void setChecked(boolean checked) {
    super.setChecked(checked);
    // Calling the super method may result in setChecked() getting called
    // recursively with a different value, so load the REAL value...
    checked = isChecked();
    if (getWindowToken() != null && ViewCompat.isLaidOut(this) && isShown()) {
        animateThumbToCheckedState(checked);
    } else {
        // Immediately move the thumb to the new position.
        cancelPositionAnimator();
        setThumbPosition(checked ? 1 : 0);
    }
}

By using a custom view extending SwitchCompat, I found out, that isShown() always returns false, because the at third iteration of the while, parent == null.

public boolean isShown() {
    View current = this;
    //noinspection ConstantConditions
    do {
        if ((current.mViewFlags & VISIBILITY_MASK) != VISIBLE) {
            return false;
        }
        ViewParent parent = current.mParent;
        if (parent == null) {
            return false; // We are not attached to the view root
        }
        if (!(parent instanceof View)) {
            return true;
        }
        current = (View) parent;
    } while (current != null);

    return false;
}

Interestingly, the third parent is the second attribute passed to getView(View convertView, ViewGroup parent) in Preference, means the PreferenceGroupAdapter didn't get a parent passed to its own getView(). Why this happens exactly and why this happens only for custom preference classes, I don't know.

For my testing purposes, I used the CheckBoxPreference with a SwitchCompat as widgetLayout, and I also didn't see animations.

Fix

Now to the fix: simply make your own view extending SwitchCompat, and override your isShown() like this:

@Override
public boolean isShown() {
    return getVisibility() == VISIBLE;
}

Use this SwitchView for your widgetLayout style, and animations work again :D

Styles:

<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
    …
    <item name="android:checkBoxPreferenceStyle">@style/Preference.SwitchView</item>
    …
</style>

<style name="Preference.SwitchView">
    <item name="android:widgetLayout">@layout/preference_switch_view</item>
</style>

Widget layout:

<de.Maxr1998.example.preference.SwitchView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/checkbox"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@null"
    android:clickable="false"
    android:focusable="false" />
查看更多
Melony?
3楼-- · 2019-01-26 10:39

i manage to fix it like this and animations is working before it was going to the state directly without animation:

FIX:

CustomSwitchCompat.class

public class CustomSwitchCompat extends SwitchCompat {

    public CustomSwitchCompat(Context context) {
        super(context);
    }

    public CustomSwitchCompat(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    @Override
    public boolean isShown() {
        return getVisibility() == VISIBLE;
    }

}

In your layout do this: preference_switch_layout.xml

<com.example.CustomSwitchCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@android:id/checkbox"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@null"
    android:clickable="false"
    android:focusable="false"
    app:switchMinWidth="55dp"/>

and in your preference.xml do this:

<CheckBoxPreference
   android:defaultValue="false"
   android:key=""
   android:widgetLayout="@layout/preference_switch_layout"
   android:summary=""
   android:title="" />
查看更多
家丑人穷心不美
4楼-- · 2019-01-26 10:41

public class SwitchPreference extends android.preference.SwitchPreference {

public SwitchPreference(Context context) {
    this(context, null);
}

public SwitchPreference(Context context, AttributeSet attrs) {
    this(context, attrs, android.R.attr.checkBoxPreferenceStyle);
}

public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
}

public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);

    try {
        Field canRecycleLayoutField = Preference.class.getDeclaredField("mCanRecycleLayout");
        canRecycleLayoutField.setAccessible(true);
        canRecycleLayoutField.setBoolean(this, true);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}
查看更多
够拽才男人
5楼-- · 2019-01-26 10:43

Sometimes Extending from a Class is not the best solution. To avoid loosing the animations you could instead Compose it, I meant creating a Class where you have a SwitchPreference field variable and apply the new logic to it. It's like a wrapper. This worked for me.

查看更多
登录 后发表回答