3

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?

TylerH
  • 20,799
  • 66
  • 75
  • 101
Billy Ryan
  • 101
  • 2
  • 4
  • i would suggest you to use CardView https://developer.android.com/reference/android/support/v7/widget/CardView.html – Amrut Bidri Mar 11 '15 at 05:12
  • I am using cardview layouts inside my recycler view. What concerns me is the over scroll indicator color when a user scrolls past the top or bottom edges of the recyclerview. – Billy Ryan Mar 11 '15 at 05:24

5 Answers5

7

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));
      }
});
Tim Malseed
  • 6,003
  • 6
  • 48
  • 66
5

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)

Lukas Novak
  • 1,149
  • 11
  • 11
  • 3
    Your solutions do not answer the question because it is about how to do it on pre-lollipop when using appcompat and your solution is only for Lollipop. – Tomáš Linhart Apr 08 '15 at 12:33
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);
}
Tomáš Linhart
  • 13,509
  • 5
  • 51
  • 54
1

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 { *; }
Lukas Novak
  • 1,149
  • 11
  • 11
  • I've tried your utility class and it worked! The weird thing was I only applied it to one recycler view as a test but it worked for all the rest components like view pager, tabs etc. Looks like all those components use just one drawable for overscroll so you can just change the color of that one drawable. Best of all no reflection needed (kind of). See this answer http://stackoverflow.com/questions/14940882/android-viewpager-edgeeffect-custom-color/20478803#20478803 – Nemanja Kovacevic Aug 26 '16 at 14:54
  • I've edited code for `setEdgeEffectColor` for NestedScrollView in recent support library. NestedScrollView now using `EdgeEffect` directly instead of `EdgeEffectCompat`, but solution above working with both approaches. – Lukas Novak Aug 18 '17 at 13:00
0

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.

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

terencey
  • 3,282
  • 4
  • 32
  • 40