Ripple Effect简单实现

需求

现在Material Design很火,而Ripple Effect即涟漪效果是能给用户点击确认感的重要UI效果。也就是说,对于可以点击的东西,都可以搞一个这种效果来加强反馈。

问题与分析

众所周知,MD是API19之后才正式提出来的,而比较完备要到API21也就是Lollipop。Ripple Effect也是如此。对于API>=21的,只需要设置background为ripple drawable,或者是?attr/selectableItemBackground,但是对于API<21的设备怎么处理呢?
Ripple是没办法了,不过可以退而求其次,至少按下去要变一个颜色吧。也就是可以搞一个Selector Drawable。
于是基本思路就有了:判断设备的API,如果是新的那就设置为Ripple,否则设置为Selector。
但是这样新问题来了:设置有两种途径,XML和Java,选择哪种呢?

  1. XML
    选择XML的话,就需要准备两个Drawable,一个放在/drawable文件夹,另一个放在v21的drawable文件夹,这样当设备API>=21的话就会采用那个文件夹里面的内容。两个Drawable名字要一样。
    这样有一个比较繁琐的地方就是每个Button都得自己设置颜色,包括Ripple和clicked之后的颜色。另外之后假如要改颜色,也得全部改。总而言之就是比较琐碎。
  2. Java
    可以选择在程序里面实时设置。相对难度要大一点,因为要自己根据颜色来生成Drawable然后设置,好处就是不用动XML,假如封装得好,一行代码就可以搞定。当然我说的不是实现的类。

代码

/**
 * Factory is used for creating colored elements for the whole app. 
* Instantiates using the {@code Singleton} pattern, with the {@code get()} method. * * @version 1.3 * @see ColorStateList * @see StateListDrawable * @see Color */ public class Coloring { private static final int BOUNDS = 1500; private static final int BRIGHTNESS_THRESHOLD = 180; private static final int FADE_DURATION = 200; private static final Object mInitializerLock; private static Coloring mInstance; static { mInitializerLock = new Object(); } /** * Destroys everything related to coloring.
*/ public static synchronized void destroy() { mInstance = null; } /** * Returns the singleton factory object. * * @return The only available {@code Coloring} */ public static Coloring get() { if (mInstance == null) { synchronized (mInitializerLock) { if (mInstance == null) { mInstance = new Coloring(); } } } return mInstance; } /* ********** Factory methods go below this line ********** */ /** * Converts a String hex color value to an Integer color value.
*
* Supported formats:
*
    *
  • #aaRRggBb
  • *
  • 0xaaRRggBb
  • *
  • 0XaaRRggBb
  • *
  • #RRggBb
  • *
  • 0xRRggBb
  • *
  • 0XRRggBb
  • *
* * @param colorString String value of the desired color * @return Integer value for the color, or gray if something goes wrong */ public int decodeColor(String colorString) { if (colorString == null || colorString.trim().isEmpty()) return Color.BLACK; if (colorString.startsWith("#")) colorString = colorString.replace("#", ""); if (colorString.startsWith("0x")) colorString = colorString.replace("0x", ""); if (colorString.startsWith("0X")) colorString = colorString.replace("0X", ""); int alpha = -1, red = -1, green = -1, blue = -1; try { if (colorString.length() == 8) { alpha = Integer.parseInt(colorString.substring(0, 2), 16); red = Integer.parseInt(colorString.substring(2, 4), 16); green = Integer.parseInt(colorString.substring(4, 6), 16); blue = Integer.parseInt(colorString.substring(6, 8), 16); } else if (colorString.length() == 6) { alpha = 255; red = Integer.parseInt(colorString.substring(0, 2), 16); green = Integer.parseInt(colorString.substring(2, 4), 16); blue = Integer.parseInt(colorString.substring(4, 6), 16); } return Color.argb(alpha, red, green, blue); } catch (NumberFormatException e) { Timber.e("Error parsing color " + e); return Color.GRAY; } } /** * Blends given color with white background. This means that a full color
* with transparency (alpha) will be lightened to make it look like it is
* rendered over a white background. Resulting color will be non-transparent. * * @param color Color to use for blending * @return Lightened color to match a white underlay render */ public int alphaBlendWithWhite(int color) { float alpha = Color.alpha(color) / 255f; int origR = Color.red(color); int origG = Color.green(color); int origB = Color.blue(color); int white = 255; // rule: outputRed = (foregroundRed * foregroundAlpha) + (backgroundRed * (1.0 - foregroundAlpha)) int r = (int) ((origR * alpha) + (white * (1.0 - alpha))); if (r > 255) r = 255; int g = (int) ((origG * alpha) + (white * (1.0 - alpha))); if (g > 255) g = 255; int b = (int) ((origB * alpha) + (white * (1.0 - alpha))); if (b > 255) b = 255; return Color.argb(255, r, g, b); } /** * Makes the given color a little bit darker. * * @param color Original color that needs to be darker * @return Darkened original color */ public int darkenColor(int color) { int amount = 30; int r = Color.red(color); int g = Color.green(color); int b = Color.blue(color); int a = Color.alpha(color); if (r - amount >= 0) { r -= amount; } else { r = 0; } if (g - amount >= 0) { g -= amount; } else { g = 0; } if (b - amount >= 0) { b -= amount; } else { b = 0; } return Color.argb(a, r, g, b); } /** * Makes the given color a little bit lighter. * * @param color Original color that needs to be lighter * @return Lightened original color */ public int lightenColor(int color) { int amount = 60; int r = Color.red(color); int g = Color.green(color); int b = Color.blue(color); int a = Color.alpha(color); if (r + amount <= 255) { r += amount; } else { r = 255; } if (g + amount <= 255) { g += amount; } else { g = 255; } if (b + amount <= 255) { b += amount; } else { b = 255; } return Color.argb(a, r, g, b); } /** * Creates a new drawable (implementation of the Drawable object may vary depending on OS version).
* Drawable will be colored with given color, and clipped to match given boundaries. * * @param color Integer color used to color the output drawable * @param bounds Four-dimensional vector bounds * @return Colored and clipped drawable object */ @SuppressWarnings("UnusedDeclaration") public Drawable createDrawable(int color, Rect bounds) { // init normal state drawable Drawable drawable = new GradientDrawable(Orientation.BOTTOM_TOP, new int[]{ color, color }).mutate(); if (color == Color.TRANSPARENT) { drawable.setAlpha(0); } drawable.setBounds(bounds); return drawable; } /** * Colors the given drawable to a specified color. Uses mode SRC_ATOP. * * @param context Which context to use * @param drawable Which drawable to color * @param color Which color to use * @return A colored drawable ready for use */ public Drawable colorDrawable(Context context, Drawable drawable, int color) { if (!(drawable instanceof BitmapDrawable)) { Timber.w("Original drawable is not a bitmap! Trying with constant state cloning."); return colorUnknownDrawable(drawable, color); } Bitmap original = ((BitmapDrawable) drawable).getBitmap(); Bitmap copy = Bitmap.createBitmap(original.getWidth(), original.getHeight(), original.getConfig()); Paint paint = new Paint(); Canvas c = new Canvas(copy); paint.setColorFilter(new PorterDuffColorFilter(color, SRC_ATOP)); c.drawBitmap(original, 0, 0, paint); return new BitmapDrawable(context.getResources(), copy); } /** * Colors the given drawable to a specified color set using the drawable wrapping technique. * * @param drawable Which drawable to color * @param colorStates Which color set to use * @return A colored drawable ready to use */ public Drawable colorDrawableWrap(Drawable drawable, ColorStateList colorStates) { if (drawable != null) { drawable = DrawableCompat.wrap(drawable); DrawableCompat.setTintList(drawable, colorStates); DrawableCompat.setTintMode(drawable, PorterDuff.Mode.SRC_ATOP); drawable = DrawableCompat.unwrap(drawable); return drawable; } return null; } /** * Colors the given drawable to a specified color using the drawable wrapping technique. * * @param drawable Which drawable to color * @param color Which color to use * @return A colored drawable ready to use */ public Drawable colorDrawableWrap(Drawable drawable, int color) { if (drawable != null) { Drawable wrapped = DrawableCompat.wrap(drawable); DrawableCompat.setTint(wrapped, color); DrawableCompat.setTintMode(wrapped, PorterDuff.Mode.SRC_ATOP); return DrawableCompat.unwrap(wrapped); } return null; } /** * Tries to clone and just color filter the drawable. Uses mode SRC_ATOP. * * @param drawable Which drawable to color * @param color Which color to use * @return A colored drawable ready for use */ @SuppressWarnings("RedundantCast") public Drawable colorUnknownDrawable(Drawable drawable, int color) { if (drawable instanceof DrawableWrapper || drawable instanceof android.support.v7.graphics.drawable.DrawableWrapper) { drawable = DrawableCompat.wrap(drawable); DrawableCompat.setTint(drawable, color); DrawableCompat.setTintMode(drawable, PorterDuff.Mode.SRC_ATOP); drawable = DrawableCompat.unwrap(drawable); return drawable; } else { try { Drawable copy = drawable.getConstantState().newDrawable(); copy.mutate(); copy.setColorFilter(color, SRC_ATOP); return copy; } catch (Exception e) { Timber.d("Failed to color unknown drawable: " + drawable.getClass().getSimpleName()); return drawable; } } } /** * Colors the given drawable to a specified color. Uses mode SRC_ATOP.
* Automatically loads a good quality bitmap from the {@code resourceId} if it is valid. * * @param context Which context to use * @param resourceId Which drawable resource to load * @param color Which color to use * @return A colored {@link Drawable} ready for use */ public Drawable colorDrawable(Context context, int resourceId, int color) { BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inDither = false; // disable dithering //noinspection deprecation opts.inPurgeable = true; // allocate pixels that could be freed by the system //noinspection deprecation opts.inInputShareable = true; // see javadoc opts.inTempStorage = new byte[32 * 1024]; // temp storage - advice is to use 16K opts.inPreferQualityOverSpeed = false; Bitmap original = BitmapFactory.decodeResource(context.getResources(), resourceId, opts); return colorDrawable(context, new BitmapDrawable(context.getResources(), original), color); } /** * Creates a new {@code StateListDrawable} drawable. States that should be provided are "normal",
* "clicked" (pressed) and "checked" (selected). All states are actually integer colors.
* Optionally, {@code shouldFade} can be set to false to avoid the fading effect.
*
* Note: {@link Color#TRANSPARENT} can be used to supply a transparent state. * * @param normal Color for the idle state * @param clicked Color for the clicked/pressed state * @param checked Color for the checked/selected state * @param shouldFade Set to true to enable the fading effect, false otherwise * @return A {@link StateListDrawable} drawable object ready for use */ @SuppressLint({ "InlinedApi", "NewApi" }) public Drawable createStateDrawable(int normal, int clicked, int checked, boolean shouldFade) { // init state arrays int[] selectedState = new int[]{ android.R.attr.state_selected }; int[] pressedState = new int[]{ android.R.attr.state_pressed }; int[] checkedState = new int[]{ android.R.attr.state_checked }; int[] focusedState = new int[]{ android.R.attr.state_focused }; int[] activatedState = new int[]{}; activatedState = new int[]{ android.R.attr.state_activated }; // init normal state drawable Drawable normalDrawable = new GradientDrawable(Orientation.BOTTOM_TOP, new int[]{ normal, normal }).mutate(); if (normal == Color.TRANSPARENT) normalDrawable.setAlpha(0); else normalDrawable.setBounds(BOUNDS, BOUNDS, BOUNDS, BOUNDS); // init clicked state drawable Drawable clickedDrawable = new GradientDrawable(Orientation.BOTTOM_TOP, new int[]{ clicked, clicked }).mutate(); if (clicked == Color.TRANSPARENT) clickedDrawable.setAlpha(0); else clickedDrawable.setBounds(BOUNDS, BOUNDS, BOUNDS, BOUNDS); // init checked state drawable Drawable checkedDrawable = new GradientDrawable(Orientation.BOTTOM_TOP, new int[]{ checked, checked }).mutate(); if (checked == Color.TRANSPARENT) checkedDrawable.setAlpha(0); else checkedDrawable.setBounds(BOUNDS, BOUNDS, BOUNDS, BOUNDS); // init focused state drawable (use normal color) Drawable focusedDrawable = new GradientDrawable(Orientation.BOTTOM_TOP, new int[]{ normal, normal }).mutate(); if (normal == Color.TRANSPARENT) focusedDrawable.setAlpha(0); else focusedDrawable.setBounds(BOUNDS, BOUNDS, BOUNDS, BOUNDS); // prepare state list (order of adding states is important!) StateListDrawable states = new StateListDrawable(); states.addState(pressedState, clickedDrawable); if (!shouldFade) { states.addState(selectedState, clickedDrawable); states.addState(focusedState, focusedDrawable); states.addState(checkedState, checkedDrawable); } // add fade effect if applicable if (shouldFade) { states.addState(new int[]{}, normalDrawable); states.setEnterFadeDuration(0); states.setExitFadeDuration(FADE_DURATION); } else { states.addState(activatedState, clickedDrawable); states.addState(new int[]{}, normalDrawable); } return states; } /** * Creates a new {@code RippleDrawable} used in Lollipop's ListView. * * @param context Which context to use * @param rippleColor Color for the clicked, pressed and focused ripple states * @return A fully colored RippleDrawable instance */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public Drawable createListViewRipple(Context context, int rippleColor) { RippleDrawable ripple; ripple = (RippleDrawable) context.getResources().getDrawable(R.drawable.list_selectors, null); if (ripple != null) { ripple.setColor(ColorStateList.valueOf(rippleColor)); } return ripple; } /** * Creates a new {@code RippleDrawable} used in Lollipop and later. * * @param normalColor Color for the idle ripple state * @param rippleColor Color for the clicked, pressed and focused ripple states * @param bounds Clip/mask drawable to these rectangle bounds * @return A fully colored RippleDrawable instance */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public Drawable createRippleDrawable(int normalColor, int rippleColor, Rect bounds) { ColorDrawable maskDrawable = null; if (bounds != null) { maskDrawable = new ColorDrawable(Color.WHITE); maskDrawable.setBounds(bounds); } if (normalColor == Color.TRANSPARENT) { return new RippleDrawable(ColorStateList.valueOf(rippleColor), null, maskDrawable); } else { return new RippleDrawable(ColorStateList.valueOf(rippleColor), new ColorDrawable(normalColor), maskDrawable); } } /** * Creates a new drawable using given parameters. States that should be provided are "normal",
* "clicked" (pressed) and "checked" (selected). All states are actually integer colors.
* Optionally, {@code shouldFade} can be set to false to avoid the fading effect.
* Depending on API level, Drawable instance will be a Ripple drawable (Lollipop) or StateListDrawable.
*
* Note: {@link Color#TRANSPARENT} can be used to supply a transparent state. * * @param normal Color for the idle state * @param clicked Color for the clicked/pressed state * @param checked Color for the checked/selected state * @param shouldFade Set to true to enable the fading effect, false otherwise * @return A {@link StateListDrawable} drawable object ready for use */ public Drawable createBackgroundDrawable(int normal, int clicked, int checked, boolean shouldFade) { return createBackgroundDrawable(normal, clicked, checked, shouldFade, null); } /** * Very similar to {@link #createBackgroundDrawable(int, int, int, boolean)}, adding only one more parameter. * * @param bounds Clip/mask drawable to these rectangle bounds * @return Clipped/masked drawable instance */ public Drawable createBackgroundDrawable(int normal, int clicked, int checked, boolean shouldFade, Rect bounds) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { return createRippleDrawable(normal, clicked, bounds); } else { return createStateDrawable(normal, clicked, checked, shouldFade); } } /** * Similar to {@link #createContrastStateDrawable(Context, int, int, boolean, android.graphics.drawable.Drawable)} but using colors * only, no drawables. * * @param normal Color normal state to this color * @param clickedBackground Background color of the View that will show when view is clicked * @return The color state list that is in contrast with the on-click background color */ @SuppressLint({ "InlinedApi", "NewApi" }) public ColorStateList createContrastStateColors(int normal, int clickedBackground) { // init state arrays int[] normalState = new int[]{}; int[] selectedState = new int[]{ android.R.attr.state_selected }; int[] pressedState = new int[]{ android.R.attr.state_pressed }; int[] checkedState = new int[]{ android.R.attr.state_checked }; int[] activatedState = new int[]{}; activatedState = new int[]{ android.R.attr.state_activated }; // initialize identifiers int[] stateColors; int[][] stateIdentifiers; int contrastColor = getContrastColor(clickedBackground); stateIdentifiers = new int[][]{ selectedState, pressedState, checkedState, activatedState, normalState }; stateColors = new int[]{ contrastColor, contrastColor, contrastColor, contrastColor, normal }; return new ColorStateList(stateIdentifiers, stateColors); } /** * Similar to {@link #createBackgroundDrawable(int, int, int, boolean)} but with additional {@code original} drawable parameter. * * @param context Which context to use * @param normal Color normal state of the drawable to this color * @param clickedBackground Background color of the View that will show when view is clicked * @param shouldFade Set to true if the state list should have a fading effect * @param original This drawable will be contrasted to the {@code clickedBackground} color on press * @return The state list drawable that is in contrast with the on-click background color */ @SuppressLint({ "InlinedApi", "NewApi" }) public Drawable createContrastStateDrawable(Context context, int normal, int clickedBackground, boolean shouldFade, Drawable original) { if (original == null || original instanceof StateListDrawable) { if (original != null) { Timber.i("Original drawable is already a StateListDrawable"); original = original.getCurrent(); } // overridden in previous if clause, so check again if (original == null) { return null; } } // init state arrays int[] selectedState = new int[]{ android.R.attr.state_selected }; int[] pressedState = new int[]{ android.R.attr.state_pressed }; int[] checkedState = new int[]{ android.R.attr.state_checked }; int[] activatedState = new int[]{}; activatedState = new int[]{ android.R.attr.state_activated }; Drawable normalStateDrawable = colorDrawable(context, original, normal); Drawable clickedStateDrawable = colorDrawable(context, original, getContrastColor(clickedBackground)); Drawable checkedStateDrawable = colorDrawable(context, original, getContrastColor(clickedBackground)); // prepare state list (order of adding states is important!) StateListDrawable states = new StateListDrawable(); states.addState(pressedState, clickedStateDrawable); if (!shouldFade) { states.addState(selectedState, clickedStateDrawable); states.addState(checkedState, checkedStateDrawable); } // add fade effect if applicable if (shouldFade) { states.addState(new int[]{}, normalStateDrawable); states.setEnterFadeDuration(0); states.setExitFadeDuration(FADE_DURATION); } else { states.addState(activatedState, clickedStateDrawable); states.addState(new int[]{}, normalStateDrawable); } return states; } /** * Very similar to {@link #createContrastStateDrawable(Context context, int, int, boolean, android.graphics.drawable.Drawable)} but * creates a Ripple drawable available in Lollipop. * * @param normal Color normal state of the drawable to this color * @param clickedBackground Background color of the View that will show when view is clicked * @param original This drawable will be contrasted to the {@code clickedBackground} color on press * @return The Ripple drawable that is in contrast with the on-click background color */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public Drawable createContrastRippleDrawable(int normal, int clickedBackground, Drawable original) { if (original == null) { Timber.i("Creating a boundless drawable for contrast ripple request - original was null!"); return createRippleDrawable(normal, clickedBackground, null); } return new RippleDrawable(ColorStateList.valueOf(clickedBackground), original, new ColorDrawable(clickedBackground)); } /** * This basically chooses between {@link #createContrastStateDrawable(Context, int, int, boolean, android.graphics.drawable.Drawable)} * and {@link #createContrastRippleDrawable(int, int, android.graphics.drawable.Drawable)} depending on the available API level. * * @param context Which context to use * @param normal Color normal state of the drawable to this color * @param clickedBackground Background color of the View that will show when view is clicked * @param shouldFade Set to true if the state list (pre-API 21) should have a fading effect * @param original This drawable will be contrasted to the {@code clickedBackground} color on press (pre-API 21) or used for masking in * ripples on post-API 21 * @return The state list drawable (< API21) or a ripple drawable (>= API21) that is in contrast with the on-click background color */ public Drawable createContrastBackgroundDrawable(Context context, int normal, int clickedBackground, boolean shouldFade, Drawable original) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { return createContrastRippleDrawable(normal, clickedBackground, original); } else { return createContrastStateDrawable(context, normal, clickedBackground, shouldFade, original); } } /** * Calculates the contrasted color from the given one. If the color darkness is under
* the {@link #BRIGHTNESS_THRESHOLD}, contrasted color is white. If the color darkness is
* over the {@link #BRIGHTNESS_THRESHOLD}, contrasted color is black. * * @param color Calculating contrasted color to this one * @return White or black, depending on the provided color's brightness */ public int getContrastColor(int color) { int r = Color.red(color); int g = Color.green(color); int b = Color.blue(color); // human eye is least sensitive to blue, then to red, then green; calculating: int brightness = (b + r + r + g + g + g) / 6; if (brightness < BRIGHTNESS_THRESHOLD) return Color.WHITE; else return Color.BLACK; } /** * Fetch the button color for you and create drawable * If transparent, then set ripple or clicked state color to grey * * @param button Target you want to set ripple on */ public void setButtonRipple(Button button) { if (button != null) { Drawable drawable; int color = this.getColorFromView(button); if (color == Color.TRANSPARENT) { drawable = this.createBackgroundDrawable(color, Color.parseColor("#FFD8D8D8"), Color.parseColor("#FFD8D8D8"), true, getRect(button)); } else { drawable = this.createBackgroundDrawable(color, this.darkenColor(color), this.darkenColor(color), true, getRect(button)); } button.setBackgroundDrawable(drawable); } } public void setLayoutRipple(ViewGroup viewGroup) { if (viewGroup != null) { Drawable drawable; int color = this.getColorFromView(viewGroup); if (color == Color.TRANSPARENT) { drawable = this.createBackgroundDrawable(color, Color.parseColor("#FFD8D8D8"), Color.parseColor("#FFD8D8D8"), true, getRect(viewGroup)); } else { drawable = this.createBackgroundDrawable(color, this.darkenColor(color), this.darkenColor(color), true, getRect(viewGroup)); } viewGroup.setBackgroundDrawable(drawable); } } private Rect getRect(View view) { int[] l = new int[2]; view.getLocationOnScreen(l); return new Rect(l[0], l[1], l[0] + view.getWidth(), l[1] + view.getHeight()); } }

这是一个帮助类,此外还需要两个Drawable:
drawable-v21/list_selectors.xml:




    
        
            
        
    


drawable/list_selectors.xml:




    
    


还有#FFF0F0F0加到Colors.xml里面去。
这个类我大部分也是参考GitHub上的,并非原创,不过后几个函数是我加的,可以直接给button和layout加ripple。理论上只要view支持background为color,都可以这样做。
这样在代码里面找到button之后,直接:

Coloring.get().setButtonRipple(mBtnView);

就可以了。

总结

颜色值是从button里面提取的,这一步可能并不高效,因为事先就已经知道了。不过我是懒得再一个个button去检查原来是什么颜色了,而且这样做的话button颜色要改这个就得一起改。

你可能感兴趣的:(Ripple Effect简单实现)