Set RecyclerView edge glow pre-lollipop when using

2019-03-22 00:32发布

问题:

I'm looking for a way to style the color of the over scroll indicator in a RecyclerView pre-lollip when using the appcompat material theme.

Internally it uses an EdgeEffect set to a internal styleable attribute that can't be set unless your already on lollipop (ironic).

Using reflection doesn't work, setting the color of the EdgeEffect is only possible on lollipop too.

On my API21 app it draws from the primary material color, on Kitkat it is white, before that it is holo blue and I'm looking to unify my design.

Any ideas on how its done?

回答1:

Use the following to set the edge effect glow color. Works on all platform versions which support EdgeEffect (API 14+), fails silently otherwise.

void themeRecyclerView(Context context, RecyclerView recyclerView) {
    int yourColor = Color.parseColor("#your_color");
    try {
        final Class<?> clazz = RecyclerView.class;
        for (final String name : new String[]{"ensureTopGlow", "ensureBottomGlow", "ensureLeftGlow", "ensureRightGlow"}) {
            Method method = clazz.getDeclaredMethod(name);
            method.setAccessible(true);
            method.invoke(recyclerView);
        }
        for (final String name : new String[]{"mTopGlow", "mBottomGlow", "mRightGlow", "mLeftGlow"}) {
            final Field field = clazz.getDeclaredField(name);
            field.setAccessible(true);
            final Object edge = field.get(recyclerView);
            final Field fEdgeEffect = edge.getClass().getDeclaredField("mEdgeEffect");
            fEdgeEffect.setAccessible(true);
            setEdgeEffectColor((EdgeEffect) fEdgeEffect.get(edge), yourColor);
        }
    } catch (final Exception | NoClassDefFoundError ignored) {
    }
}

void setEdgeEffectColor(EdgeEffect edgeEffect, int color) {
    try {
        if (Build.VERSION.SDK_INT >= 21) {
            edgeEffect.setColor(color);
            return;
        }

        for(String name : new String[]{"mEdge", "mGlow"}){
            final Field field = EdgeEffect.class.getDeclaredField(name);
            field.setAccessible(true);
            final Drawable drawable = (Drawable) field.get(edgeEffect);
            drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
            drawable.setCallback(null);
        }
    } catch (final Exception | NoClassDefFoundError ignored) {
    }
}

Thanks @Lukas Novak for providing the majority of this code..

As Lukas said, these methods must be called from within an onScrollListener on your RecyclerView:

recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
      @Override
      public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
           super.onScrollStateChanged(recyclerView, newState);
           EdgeChanger.setEdgeGlowColor(recycler, getResources().getColor(R.color.your_color));
      }
});


回答2:

Thanks @Tomáš Linhart for pointing it out. Solution below is for changing edge color only in API >21. It can be used with AppCompat, but effect of changing color will be visible only in Lollipop and above.


I've found a way to set color with use of reflection. For example here is code for change top and bottom edge color:

public static void setEdgeGlowColor(final RecyclerView recyclerView, final int color) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        try {
            final Class<?> clazz = RecyclerView.class;
            for (final String name : new String[] {"ensureTopGlow", "ensureBottomGlow"}) {
                Method method = clazz.getDeclaredMethod(name);
                method.setAccessible(true);
                method.invoke(recyclerView);
            }
            for (final String name : new String[] {"mTopGlow", "mBottomGlow"}) {
                final Field field = clazz.getDeclaredField(name);
                field.setAccessible(true);
                final Object edge = field.get(recyclerView); // android.support.v4.widget.EdgeEffectCompat
                final Field fEdgeEffect = edge.getClass().getDeclaredField("mEdgeEffect");
                fEdgeEffect.setAccessible(true);
                ((EdgeEffect) fEdgeEffect.get(edge)).setColor(color);
            }
        } catch (final Exception ignored) {}
    }
}

Unlike solutions with other components like ListView or ScrollView, here you must call package-private methods ensureTopGlow, ensureBottomGlow, etc. and call setEdgeEffectColor(RecyclerView recycler, int color) above in onScrollStateChanged method of RecyclerView.OnScrollListener.

For example:

recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
      @Override
      public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
           super.onScrollStateChanged(recyclerView, newState);
           EdgeChanger.setEdgeGlowColor(recycler, getResources().getColor(R.color.your_color));
      }
});

By default, Android calls ensure*Glow methods with start of scrolling. In these methods there is initialized new EdgeEffect with default color, but only if it is not initialized yet. To prevent this behaviour you must call ensure*Glow methods and then change the color of edge, so subsequent initializing of EdgeEffect will be ignored (as in setEdgeGlowColor method above)



回答3:

EdgeEffect is using a drawable so you can change the drawable as it is described in this article but it will affect all EdgeEffect classes in your context.

Basically it is only about introducing and calling this method but there are some pitfalls that are described the article so I suggest you to read it first.

static void brandGlowEffect(Context context, int brandColor) {
      //glow
      int glowDrawableId = context.getResources().getIdentifier("overscroll_glow", "drawable", "android");
      Drawable androidGlow = context.getResources().getDrawable(glowDrawableId);
      androidGlow.setColorFilter(brandColor, PorterDuff.Mode.SRC_IN);
      //edge
      int edgeDrawableId = context.getResources().getIdentifier("overscroll_edge", "drawable", "android");
      Drawable androidEdge = context.getResources().getDrawable(edgeDrawableId);
      androidEdge.setColorFilter(brandColor, PorterDuff.Mode.SRC_IN);
}


回答4:

I've been unable to find a way to set the overscroll colour for RecyclerViews too.

So a possible solution is to have different layout files for pre-v21 and post-v21.

  • Pre-v21: use listviews and set the overscroll colour via https://github.com/AndroidAlliance/EdgeEffectOverride
  • Post-v21: use recyclerviews and set the overscroll colour via colorAccent

The disadvantage is that your code will be messy and you need to have two different adapters for recyclerview/listview.



回答5:

I wrote utility class EdgeChanger, which is mixture of my previous post, @Jared Hummler code and @Eugen Pechanec code.

This utility class use reflection to change color of edge glow for

ScrollView, NestedScrollView, ListView, ViewPager and RecyclerView

and works with Marshmallow, Lollipop and pre-Lollipop devices when using AppCompat so you don't need to use third party libraries like EdgeEffectOverride or use different layouts.

Use this only when you want to change edge glow colors after onCreate(), otherwise you should use setTheme and different themes with different color attribute colorPrimary or colorEdgeEffect.

public class EdgeChanger {

private static final Class<?> CLASS_SCROLL_VIEW = ScrollView.class;
private static Field SCROLL_VIEW_FIELD_EDGE_GLOW_TOP;
private static Field SCROLL_VIEW_FIELD_EDGE_GLOW_BOTTOM;

private static final Class<?> CLASS_LIST_VIEW = AbsListView.class;
private static Field LIST_VIEW_FIELD_EDGE_GLOW_TOP;
private static Field LIST_VIEW_FIELD_EDGE_GLOW_BOTTOM;

private static final Class<?> CLASS_NESTED_SCROLL_VIEW = NestedScrollView.class;
private static Field NESTED_SCROLL_VIEW_FIELD_EDGE_GLOW_TOP;
private static Field NESTED_SCROLL_VIEW_FIELD_EDGE_GLOW_BOTTOM;
private static Method NESTED_SCROLL_VIEW_METHOD_ENSURE_GLOWS;

private static final Class<?> CLASS_RECYCLER_VIEW = RecyclerView.class;
private static Field RECYCLER_VIEW_FIELD_EDGE_GLOW_TOP;
private static Field RECYCLER_VIEW_FIELD_EDGE_GLOW_BOTTOM;
private static Method RECYCLER_VIEW_METHOD_ENSURE_GLOW_TOP;
private static Method RECYCLER_VIEW_METHOD_ENSURE_GLOW_BOTTOM;

private static final Class<?> CLASS_VIEW_PAGER = ViewPager.class;
private static Field VIEW_PAGER_FIELD_EDGE_GLOW_LEFT;
private static Field VIEW_PAGER_FIELD_EDGE_GLOW_RIGHT;

static {

    Field edgeGlowTop = null, edgeGlowBottom = null;
    Method ensureGlowTop = null, ensureGlowBottom = null;

    for (Field f : CLASS_SCROLL_VIEW.getDeclaredFields()) {
        switch (f.getName()) {
            case "mEdgeGlowTop":
                f.setAccessible(true);
                edgeGlowTop = f;
                break;
            case "mEdgeGlowBottom":
                f.setAccessible(true);
                edgeGlowBottom = f;
                break;
        }
    }
    SCROLL_VIEW_FIELD_EDGE_GLOW_TOP = edgeGlowTop;
    SCROLL_VIEW_FIELD_EDGE_GLOW_BOTTOM = edgeGlowBottom;

    for (Field f : CLASS_LIST_VIEW.getDeclaredFields()) {
        switch (f.getName()) {
            case "mEdgeGlowTop":
                f.setAccessible(true);
                edgeGlowTop = f;
                break;
            case "mEdgeGlowBottom":
                f.setAccessible(true);
                edgeGlowBottom = f;
                break;
        }
    }
    LIST_VIEW_FIELD_EDGE_GLOW_TOP = edgeGlowTop;
    LIST_VIEW_FIELD_EDGE_GLOW_BOTTOM = edgeGlowBottom;

    for (Field f : CLASS_NESTED_SCROLL_VIEW.getDeclaredFields()) {
        switch (f.getName()) {
            case "mEdgeGlowTop":
                f.setAccessible(true);
                edgeGlowTop = f;
                break;
            case "mEdgeGlowBottom":
                f.setAccessible(true);
                edgeGlowBottom = f;
                break;
        }
    }
    for (Method m : CLASS_NESTED_SCROLL_VIEW.getDeclaredMethods()) {
        switch (m.getName()) {
            case "ensureGlows":
                m.setAccessible(true);
                ensureGlowTop = m;
                break;
        }
    }
    NESTED_SCROLL_VIEW_FIELD_EDGE_GLOW_TOP = edgeGlowTop;
    NESTED_SCROLL_VIEW_FIELD_EDGE_GLOW_BOTTOM = edgeGlowBottom;
    NESTED_SCROLL_VIEW_METHOD_ENSURE_GLOWS = ensureGlowTop;

    for (Field f : CLASS_RECYCLER_VIEW.getDeclaredFields()) {
        switch (f.getName()) {
            case "mTopGlow":
                f.setAccessible(true);
                edgeGlowTop = f;
                break;
            case "mBottomGlow":
                f.setAccessible(true);
                edgeGlowBottom = f;
                break;
        }
    }
    for (Method m : CLASS_RECYCLER_VIEW.getDeclaredMethods()) {
        switch (m.getName()) {
            case "ensureTopGlow":
                m.setAccessible(true);
                ensureGlowTop = m;
                break;
            case "ensureBottomGlow":
                m.setAccessible(true);
                ensureGlowBottom = m;
                break;
        }
    }
    RECYCLER_VIEW_FIELD_EDGE_GLOW_TOP = edgeGlowTop;
    RECYCLER_VIEW_FIELD_EDGE_GLOW_BOTTOM = edgeGlowBottom;
    RECYCLER_VIEW_METHOD_ENSURE_GLOW_TOP = ensureGlowTop;
    RECYCLER_VIEW_METHOD_ENSURE_GLOW_BOTTOM = ensureGlowBottom;

    for (Field f : CLASS_VIEW_PAGER.getDeclaredFields()) {
        switch (f.getName()) {
            case "mLeftEdge":
                f.setAccessible(true);
                edgeGlowTop = f;
                break;
            case "mRightEdge":
                f.setAccessible(true);
                edgeGlowBottom = f;
                break;
        }
    }
    VIEW_PAGER_FIELD_EDGE_GLOW_LEFT = edgeGlowTop;
    VIEW_PAGER_FIELD_EDGE_GLOW_RIGHT = edgeGlowBottom;

}

public static void setEdgeGlowColor(AbsListView listView, int color) {

    try {
        setEdgeEffectColor(LIST_VIEW_FIELD_EDGE_GLOW_TOP.get(listView), color);
        setEdgeEffectColor(LIST_VIEW_FIELD_EDGE_GLOW_BOTTOM.get(listView), color);
    } catch (Exception e) {
        e.printStackTrace();
    }

}

public static void setEdgeGlowColor(ScrollView scrollView, int color) {

    try {
        setEdgeEffectColor(SCROLL_VIEW_FIELD_EDGE_GLOW_TOP.get(scrollView), color);
        setEdgeEffectColor(SCROLL_VIEW_FIELD_EDGE_GLOW_BOTTOM.get(scrollView), color);
    } catch (Exception e) {
        e.printStackTrace();
    }

}

public static void setEdgeGlowColor(final ViewPager viewPager, final int color) {

    try {
        setEdgeEffectColor(VIEW_PAGER_FIELD_EDGE_GLOW_LEFT.get(viewPager), color);
        setEdgeEffectColor(VIEW_PAGER_FIELD_EDGE_GLOW_RIGHT.get(viewPager), color);
    } catch (final Exception e) {
        e.printStackTrace();
    }

}

public static void setEdgeGlowColor(NestedScrollView scrollView, int color) {

    try {
        NESTED_SCROLL_VIEW_METHOD_ENSURE_GLOWS.invoke(scrollView);
        setEdgeEffectColor(NESTED_SCROLL_VIEW_FIELD_EDGE_GLOW_TOP.get(scrollView), color);
        setEdgeEffectColor(NESTED_SCROLL_VIEW_FIELD_EDGE_GLOW_BOTTOM.get(scrollView), color);
    } catch (final Exception | NoClassDefFoundError e) {
        e.printStackTrace();
    }

}

public static void setEdgeGlowColor(final RecyclerView recyclerView, final int color) {

    try {
        RECYCLER_VIEW_METHOD_ENSURE_GLOW_TOP.invoke(recyclerView);
        RECYCLER_VIEW_METHOD_ENSURE_GLOW_BOTTOM.invoke(recyclerView);
        setEdgeEffectColor(RECYCLER_VIEW_FIELD_EDGE_GLOW_TOP.get(recyclerView), color);
        setEdgeEffectColor(RECYCLER_VIEW_FIELD_EDGE_GLOW_BOTTOM.get(recyclerView), color);
    } catch (final Exception | NoClassDefFoundError e) {
        e.printStackTrace();
    }

}

private static void setEdgeEffectColor(Object object, int color) {

    try {
        EdgeEffect edgeEffect = null;
        if (object instanceof EdgeEffectCompat) {
            final Field fEdgeEffect = object.getClass().getDeclaredField("mEdgeEffect");
            fEdgeEffect.setAccessible(true);
            edgeEffect = (EdgeEffect) fEdgeEffect.get(object);
        } else if (object instanceof EdgeEffect) {
            edgeEffect = (EdgeEffect) object;
        }

        if (Build.VERSION.SDK_INT >= 21) {
            edgeEffect.setColor(color);
        } else {
            for (String name : new String[] {"mEdge", "mGlow"}) {
                final Field field = EdgeEffect.class.getDeclaredField(name);
                field.setAccessible(true);
                final Drawable drawable = (Drawable) field.get(edgeEffect);
                drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
                drawable.setCallback(null);
            }
        }
    } catch (final Exception | NoClassDefFoundError e) {
        e.printStackTrace();
    }

}

}

If you're using ProGuard, don't forget to add these rules (to not renaming fields with which you want work using reflection):

-keepnames class android.widget.ScrollView { *; }
-keepnames class android.widget.AbsListView { *; }
-keepnames class android.support.v4.widget.NestedScrollView { *; }
-keepnames class android.support.v7.widget.RecyclerView { *; }
-keepnames class android.support.v4.view.ViewPager { *; }
-keepnames class android.widget.EdgeEffect { *; }
-keepnames class android.support.v4.widget.EdgeEffectCompat { *; }