Android控件的水波纹效果的实现方式有很多种,比如使用ripple文件,这里介绍一下另一种Android原生的水波纹实现方案(API28及以上)。
我们利用RippleDrawable来实现一个带Ripple的Button。RippleDrawable可以通过xml 中定义 ripple来实现,或者通过代码中动态创建RippleDrawable设置给控件。
自定义属性
添加ripple相关的自定义属性
<declare-styleable name="shape_button">
<attr name="carbon_rippleColor" />
<attr name="carbon_rippleStyle" />
<attr name="carbon_rippleHotspot" />
<attr name="carbon_rippleRadius" />
...
</declare-styleable>
<!-- 两种类型的Ripple,background表示在控件背景上显示水波纹,borderless表示在控件背后显示水波纹 -->
<attr name="carbon_rippleStyle" format="enum">
<enum name="background" value="0" />
<enum name="borderless" value="1" />
</attr>
RippleView接口
创建RippleView通用接口
public interface RippleView {
/**
* Gets the ripple drawable.
*
* @return the ripple drawable. Can be null.
*/
RippleDrawable getRippleDrawable();
/**
* Sets the ripple drawable. This method doesn't break the background.
*
* @param rippleDrawable the ripple drawable. Can be null
*/
void setRippleDrawable(RippleDrawable rippleDrawable);
}
RippleView
接口中的RippleDrawable
也是一个接口,里面管理了RippleDrawable文件对象的创建。
RippleDrawable接口
public interface RippleDrawable{
Drawable getBackground();
enum Style {
Background, Borderless
}
boolean setState(int[] stateSet);
void draw(Canvas canvas);
Style getStyle();
boolean isHotspotEnabled();
void setHotspotEnabled(boolean useHotspot);
void setBounds(int left, int top, int right, int bottom);
void setBounds(Rect bounds);
void setHotspot(float x, float y);
boolean isStateful();
void setCallback(Drawable.Callback cb);
ColorStateList getColor();
void setRadius(int radius);
int getRadius();
static RippleDrawable create(ColorStateList color, Style style, View view, boolean useHotspot, int radius) {
RippleDrawable rippleDrawable = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
rippleDrawable = new RippleDrawableMarshmallow(color, style == Style.Background ? view.getBackground() : null, style);
} else if (Carbon.IS_LOLLIPOP_OR_HIGHER) {
rippleDrawable = new RippleDrawableLollipop(color, style == Style.Background ? view.getBackground() : null, style);
}
rippleDrawable.setCallback(view);
rippleDrawable.setHotspotEnabled(useHotspot);
rippleDrawable.setRadius(radius);
return rippleDrawable;
}
static RippleDrawable create(ColorStateList color, Style style, View view, Drawable background, boolean useHotspot, int radius) {
RippleDrawable rippleDrawable = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
rippleDrawable = new RippleDrawableMarshmallow(color, background, style);
} else if (Carbon.IS_LOLLIPOP_OR_HIGHER) {
rippleDrawable = new RippleDrawableLollipop(color, background, style);
}
rippleDrawable.setCallback(view);
rippleDrawable.setHotspotEnabled(useHotspot);
rippleDrawable.setRadius(radius);
return rippleDrawable;
}
}
通过提供出去的
create
方法可以生成一个RippleDrawable
文件对象,根据版本号生成不同的RippleDrawableMarshmallow
对象 或RippleDrawableLollipop
对象。
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class RippleDrawableMarshmallow extends android.graphics.drawable.RippleDrawable implements RippleDrawable {
private final ColorStateList color;
private final Drawable background;
private Style style;
private boolean useHotspot;
public RippleDrawableMarshmallow(ColorStateList color, Drawable background, Style style) {
super(color, background, style == Style.Borderless ? null : new ColorDrawable(0xffffffff));
this.style = style;
this.color = color;
this.background = background;
}
@Override
public Drawable getBackground() {
return background;
}
@Override
public Style getStyle() {
return style;
}
@Override
public boolean isHotspotEnabled() {
return useHotspot;
}
@Override
public void setHotspotEnabled(boolean useHotspot) {
this.useHotspot = useHotspot;
}
@Override
public ColorStateList getColor() {
return color;
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class RippleDrawableLollipop extends android.graphics.drawable.RippleDrawable implements RippleDrawable {
private final ColorStateList color;
private final Drawable background;
private Style style;
private boolean useHotspot;
private int radius;
public RippleDrawableLollipop(ColorStateList color, Drawable background, Style style) {
super(color, background, style == Style.Borderless ? null : new ColorDrawable(0xffffffff));
this.style = style;
this.color = color;
this.background = background;
}
@Override
public Drawable getBackground() {
return background;
}
@Override
public Style getStyle() {
return style;
}
@Override
public boolean isHotspotEnabled() {
return useHotspot;
}
@Override
public void setHotspotEnabled(boolean useHotspot) {
this.useHotspot = useHotspot;
}
@Override
public ColorStateList getColor() {
return color;
}
@Override
public void setRadius(int radius) {
this.radius = radius;
try {
Method setMaxRadiusMethod = android.graphics.drawable.RippleDrawable.class.getDeclaredMethod("setMaxRadius", int.class);
setMaxRadiusMethod.invoke(this, radius);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
public int getRadius() {
return radius;
}
}
ShapeButton
ShapeButton 实现RippleView接口:
public class ShapeButton extends AppCompatButton
implements ShadowView,
ShapeModelView,
RippleView {
...
private static int[] rippleIds = new int[]{
R.styleable.shape_button_carbon_rippleColor,
R.styleable.shape_button_carbon_rippleStyle,
R.styleable.shape_button_carbon_rippleHotspot,
R.styleable.shape_button_carbon_rippleRadius
};
private void initButton(AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.shape_button, defStyleAttr, defStyleRes);
Carbon.initElevation(this, a, elevationIds);
Carbon.initCornerCutRadius(this,a,cornerCutRadiusIds);
// 初始化ripple相关属性
Carbon.initRippleDrawable(this,a,rippleIds);
a.recycle();
}
// -------------------------------
// shadow
// -------------------------------
... ... ...
// -------------------------------
// shape
// -------------------------------
... ... ...
// -------------------------------
// ripple
// -------------------------------
private RippleDrawable rippleDrawable;
@Override
public boolean dispatchTouchEvent(@NonNull MotionEvent event) {
// 为rippledrawable设置扩散点
if (rippleDrawable != null && event.getAction() == MotionEvent.ACTION_DOWN)
rippleDrawable.setHotspot(event.getX(),event.getY());
return super.dispatchTouchEvent(event);
}
@Override
public RippleDrawable getRippleDrawable() {
return rippleDrawable;
}
@Override
public void setRippleDrawable(RippleDrawable newRipple) {
// todo -- 为控件设置RippleDrawable作为背景
}
}
初始化ripple相关属性:
public static void initRippleDrawable(RippleView rippleView, TypedArray a, int[] ids) {
int carbon_rippleColor = ids[0];
int carbon_rippleStyle = ids[1];
int carbon_rippleHotspot = ids[2];
int carbon_rippleRadius = ids[3];
View view = (View) rippleView;
if (view.isInEditMode())
return;
ColorStateList color = a.getColorStateList(carbon_rippleColor);
if (color != null) {
RippleDrawable.Style style = RippleDrawable.Style.values()[a.getInt(carbon_rippleStyle, RippleDrawable.Style.Background.ordinal())];
boolean useHotspot = a.getBoolean(carbon_rippleHotspot, true);
int radius = (int) a.getDimension(carbon_rippleRadius, -1);
rippleView.setRippleDrawable(RippleDrawable.create(color, style, view, useHotspot, radius));
}
}
Background
我们首先实现Style为background的情况:
@Override
public void setRippleDrawable(RippleDrawable newRipple) {
if (newRipple != null) {
newRipple.setCallback(this);
newRipple.setBounds(0, 0, getWidth(), getHeight());
newRipple.setState(getDrawableState());
((Drawable) newRipple).setVisible(getVisibility() == VISIBLE, false);
// 其实就只是为控件设置了一个background,这个background是一个rippleDrawable图片
if (newRipple.getStyle() == RippleDrawable.Style.Background)
super.setBackgroundDrawable((Drawable) newRipple);
}
rippleDrawable = newRipple;
}
如何使用:
<com.chinatsp.demo1.shadow.ShapeButton
android:id="@+id/show_dialog_btn"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_margin="@dimen/carbon_padding"
android:background="#ffffff"
android:stateListAnimator="@null"
android:text="TOM"
app:carbon_cornerRadius="10dp"
android:elevation="2dp"
app:carbon_elevationShadowColor="@color/carbon_red_700"
app:carbon_rippleColor="@color/carbon_green_700"
app:carbon_rippleStyle="background"
app:carbon_rippleRadius="40dp"
/>
Borderless
要实现borderless效果,我们首先需要为rippleDrawable添加状态,例如按压状态state_press
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (rippleDrawable != null && rippleDrawable.getStyle() != RippleDrawable.Style.Background)
rippleDrawable.setState(getDrawableState());
}
其次,borderless要求超出控件边界,其实是把子控件的rippledrawable显示在了父控件上,因此在刷新子控件背景时需要刷新父控件:
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
super.invalidateDrawable(drawable);
invalidateParentIfNeeded();
}
@Override
public void invalidate(@NonNull Rect dirty) {
super.invalidate(dirty);
invalidateParentIfNeeded();
}
@Override
public void invalidate(int l, int t, int r, int b) {
super.invalidate(l, t, r, b);
invalidateParentIfNeeded();
}
@Override
public void invalidate() {
super.invalidate();
invalidateParentIfNeeded();
}
private void invalidateParentIfNeeded() {
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).invalidate();
}
假如当前Button的父控件是LinearLayout,则需要我们重写LinearLayout的drawChild方法来显示子控件的rippleDrawable:
public class LinearLayout extends LinearLayoutCompat {
public LinearLayout(@NonNull Context context) {
super(context);
}
public LinearLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public LinearLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
if (child instanceof RippleView) {
RippleView rippleView = (RippleView) child;
RippleDrawable rippleDrawable = rippleView.getRippleDrawable();
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless) {
int saveCount = canvas.save();
canvas.translate(child.getLeft() + child.getWidth()/2f, child.getTop() + child.getHeight()/2f);
canvas.concat(child.getMatrix());
// 在子控件中间显示子控件的rippledrawable
rippleDrawable.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
return super.drawChild(canvas, child, drawingTime);
}
}
如何使用:
<com.chinatsp.demo1.ripple.LinearLayout
android:layout_width="match_parent"
android:layout_height="500dp"
android:orientation="horizontal">
<com.chinatsp.demo1.shadow.ShapeButton
android:id="@+id/show_dialog_btn"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_margin="@dimen/carbon_padding"
android:background="#ffffff"
android:stateListAnimator="@null"
android:text="TOM"
app:carbon_cornerRadius="10dp"
android:elevation="2dp"
app:carbon_elevationShadowColor="@color/carbon_red_700"
app:carbon_rippleColor="@color/carbon_green_700"
app:carbon_rippleStyle="borderless"
/>
</com.chinatsp.demo1.ripple.LinearLayout>
完整代码:
public class ShapeButton extends AppCompatButton
implements ShadowView,
ShapeModelView,
RippleView {
public ShapeButton(@NonNull Context context) {
super(context);
initButton(null, android.R.attr.buttonStyle, R.style.carbon_Button);
}
public ShapeButton(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initButton(attrs, android.R.attr.buttonStyle, R.style.carbon_Button);
}
public ShapeButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initButton(attrs, defStyleAttr, R.style.carbon_Button);
}
public ShapeButton(Context context, String text, OnClickListener listener) {
super(context);
initButton(null, android.R.attr.buttonStyle, R.style.carbon_Button);
setText(text);
setOnClickListener(listener);
}
private static int[] elevationIds = new int[]{
R.styleable.shape_button_carbon_elevation,
R.styleable.shape_button_carbon_elevationShadowColor,
R.styleable.shape_button_carbon_elevationAmbientShadowColor,
R.styleable.shape_button_carbon_elevationSpotShadowColor
};
private static int[] cornerCutRadiusIds = new int[]{
R.styleable.shape_button_carbon_cornerRadiusTopStart,
R.styleable.shape_button_carbon_cornerRadiusTopEnd,
R.styleable.shape_button_carbon_cornerRadiusBottomStart,
R.styleable.shape_button_carbon_cornerRadiusBottomEnd,
R.styleable.shape_button_carbon_cornerRadius,
R.styleable.shape_button_carbon_cornerCutTopStart,
R.styleable.shape_button_carbon_cornerCutTopEnd,
R.styleable.shape_button_carbon_cornerCutBottomStart,
R.styleable.shape_button_carbon_cornerCutBottomEnd,
R.styleable.shape_button_carbon_cornerCut
};
private static int[] rippleIds = new int[]{
R.styleable.shape_button_carbon_rippleColor,
R.styleable.shape_button_carbon_rippleStyle,
R.styleable.shape_button_carbon_rippleHotspot,
R.styleable.shape_button_carbon_rippleRadius
};
protected TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private void initButton(AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.shape_button, defStyleAttr, defStyleRes);
Carbon.initElevation(this, a, elevationIds);
Carbon.initCornerCutRadius(this,a,cornerCutRadiusIds);
Carbon.initRippleDrawable(this,a,rippleIds);
a.recycle();
}
// -------------------------------
// shadow
// -------------------------------
private float elevation = 0;
private float translationZ = 0;
private ColorStateList ambientShadowColor, spotShadowColor;
@Override
public float getElevation() {
return elevation;
}
@Override
public void setElevation(float elevation) {
if (Carbon.IS_PIE_OR_HIGHER) {
super.setElevation(elevation);
super.setTranslationZ(translationZ);
} else if (Carbon.IS_LOLLIPOP_OR_HIGHER) {
if (ambientShadowColor == null || spotShadowColor == null) {
super.setElevation(elevation);
super.setTranslationZ(translationZ);
} else {
super.setElevation(0);
super.setTranslationZ(0);
}
} else if (elevation != this.elevation && getParent() != null) {
((View) getParent()).postInvalidate();
}
this.elevation = elevation;
}
@Override
public float getTranslationZ() {
return translationZ;
}
public void setTranslationZ(float translationZ) {
if (translationZ == this.translationZ)
return;
if (Carbon.IS_PIE_OR_HIGHER) {
super.setTranslationZ(translationZ);
} else if (Carbon.IS_LOLLIPOP_OR_HIGHER) {
if (ambientShadowColor == null || spotShadowColor == null) {
super.setTranslationZ(translationZ);
} else {
super.setTranslationZ(0);
}
} else if (translationZ != this.translationZ && getParent() != null) {
((View) getParent()).postInvalidate();
}
this.translationZ = translationZ;
}
@Override
public ColorStateList getElevationShadowColor() {
return ambientShadowColor;
}
@Override
public void setElevationShadowColor(ColorStateList shadowColor) {
ambientShadowColor = spotShadowColor = shadowColor;
setElevation(elevation);
setTranslationZ(translationZ);
}
@Override
public void setElevationShadowColor(int color) {
ambientShadowColor = spotShadowColor = ColorStateList.valueOf(color);
setElevation(elevation);
setTranslationZ(translationZ);
}
@Override
public void setOutlineAmbientShadowColor(ColorStateList color) {
ambientShadowColor = color;
if (Carbon.IS_PIE_OR_HIGHER) {
super.setOutlineAmbientShadowColor(color.getColorForState(getDrawableState(), color.getDefaultColor()));
} else {
setElevation(elevation);
setTranslationZ(translationZ);
}
}
@Override
public void setOutlineAmbientShadowColor(int color) {
setOutlineAmbientShadowColor(ColorStateList.valueOf(color));
}
@Override
public int getOutlineAmbientShadowColor() {
return ambientShadowColor.getDefaultColor();
}
@Override
public void setOutlineSpotShadowColor(int color) {
setOutlineSpotShadowColor(ColorStateList.valueOf(color));
}
@Override
public void setOutlineSpotShadowColor(ColorStateList color) {
spotShadowColor = color;
if (Carbon.IS_PIE_OR_HIGHER) {
super.setOutlineSpotShadowColor(color.getColorForState(getDrawableState(), color.getDefaultColor()));
} else {
setElevation(elevation);
setTranslationZ(translationZ);
}
}
@Override
public int getOutlineSpotShadowColor() {
return ambientShadowColor.getDefaultColor();
}
@Override
public boolean hasShadow() {
return false;
}
@Override
public void drawShadow(Canvas canvas) {
}
@Override
public void draw(Canvas canvas) {
boolean c = !Carbon.isShapeRect(shapeModel, boundsRect);
if (Carbon.IS_PIE_OR_HIGHER) {
if (spotShadowColor != null)
super.setOutlineSpotShadowColor(spotShadowColor.getColorForState(getDrawableState(), spotShadowColor.getDefaultColor()));
if (ambientShadowColor != null)
super.setOutlineAmbientShadowColor(ambientShadowColor.getColorForState(getDrawableState(), ambientShadowColor.getDefaultColor()));
}
// 判断如果不是圆角矩形,需要使用轮廓Path,绘制一下Path,不然显示会很奇怪
if (getWidth() > 0 && getHeight() > 0 && ((c && !Carbon.IS_LOLLIPOP_OR_HIGHER) || !shapeModel.isRoundRect(boundsRect))) {
int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
super.draw(canvas);
paint.setXfermode(Carbon.CLEAR_MODE);
if (c) {
cornersMask.setFillType(Path.FillType.INVERSE_WINDING);
canvas.drawPath(cornersMask, paint);
}
canvas.restoreToCount(saveCount);
paint.setXfermode(null);
}else{
super.draw(canvas);
}
}
// -------------------------------
// shape
// -------------------------------
private ShapeAppearanceModel shapeModel = new ShapeAppearanceModel();
private MaterialShapeDrawable shadowDrawable = new MaterialShapeDrawable(shapeModel);
@Override
public void setShapeModel(ShapeAppearanceModel shapeModel) {
this.shapeModel = shapeModel;
shadowDrawable = new MaterialShapeDrawable(shapeModel);
if (getWidth() > 0 && getHeight() > 0)
updateCorners();
if (!Carbon.IS_LOLLIPOP_OR_HIGHER)
postInvalidate();
}
// View的轮廓形状
private RectF boundsRect = new RectF();
// View的轮廓形状形成的Path路径
private Path cornersMask = new Path();
/**
* 更新圆角
*/
private void updateCorners() {
if (Carbon.IS_LOLLIPOP_OR_HIGHER) {
// 如果不是矩形,裁剪View的轮廓
if (!Carbon.isShapeRect(shapeModel, boundsRect)){
setClipToOutline(true);
}
//该方法返回一个Outline对象,它描述了该视图的形状。
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
if (Carbon.isShapeRect(shapeModel, boundsRect)) {
outline.setRect(0, 0, getWidth(), getHeight());
} else {
shadowDrawable.setBounds(0, 0, getWidth(), getHeight());
shadowDrawable.setShadowCompatibilityMode(MaterialShapeDrawable.SHADOW_COMPAT_MODE_NEVER);
shadowDrawable.getOutline(outline);
}
}
});
}
// 拿到圆角矩形的形状
boundsRect.set(shadowDrawable.getBounds());
// 拿到圆角矩形的Path
shadowDrawable.getPathForSize(getWidth(), getHeight(), cornersMask);
}
@Override
public ShapeAppearanceModel getShapeModel() {
return this.shapeModel;
}
@Override
public void setCornerCut(float cornerCut) {
shapeModel = ShapeAppearanceModel.builder().setAllCorners(new CutCornerTreatment(cornerCut)).build();
setShapeModel(shapeModel);
}
@Override
public void setCornerRadius(float cornerRadius) {
shapeModel = ShapeAppearanceModel.builder().setAllCorners(new RoundedCornerTreatment(cornerRadius)).build();
setShapeModel(shapeModel);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!changed)
return;
if (getWidth() == 0 || getHeight() == 0)
return;
updateCorners();
}
// -------------------------------
// ripple
// -------------------------------
private RippleDrawable rippleDrawable;
@Override
public boolean dispatchTouchEvent(@NonNull MotionEvent event) {
if (rippleDrawable != null && event.getAction() == MotionEvent.ACTION_DOWN)
rippleDrawable.setHotspot(event.getX(),event.getY());
return super.dispatchTouchEvent(event);
}
@Override
public RippleDrawable getRippleDrawable() {
return rippleDrawable;
}
@Override
public void setRippleDrawable(RippleDrawable newRipple) {
if (newRipple != null) {
newRipple.setCallback(this);
newRipple.setBounds(0, 0, getWidth(), getHeight());
newRipple.setState(getDrawableState());
((Drawable) newRipple).setVisible(getVisibility() == VISIBLE, false);
if (newRipple.getStyle() == RippleDrawable.Style.Background)
super.setBackgroundDrawable((Drawable) newRipple);
}
rippleDrawable = newRipple;
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (rippleDrawable != null && rippleDrawable.getStyle() != RippleDrawable.Style.Background)
rippleDrawable.setState(getDrawableState());
}
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
super.invalidateDrawable(drawable);
invalidateParentIfNeeded();
}
@Override
public void invalidate(@NonNull Rect dirty) {
super.invalidate(dirty);
invalidateParentIfNeeded();
}
@Override
public void invalidate(int l, int t, int r, int b) {
super.invalidate(l, t, r, b);
invalidateParentIfNeeded();
}
@Override
public void invalidate() {
super.invalidate();
invalidateParentIfNeeded();
}
private void invalidateParentIfNeeded() {
if (getParent() == null || !(getParent() instanceof View))
return;
if (rippleDrawable != null && rippleDrawable.getStyle() == RippleDrawable.Style.Borderless)
((View) getParent()).invalidate();
}
}