在前面一篇博客中介绍了Android中水波纹的使用,如果忘记了可以去复习一下Android中水波纹使用,但是那种实现方法要在API21+以上才能使用。如果我们要兼容API21以下的系统,则需要通过第三方的类库,即通过自定义视图来实现水波纹的效果。
自定义视图,是Android中扩展控件常用的方法,这里不做过多的介绍,如果不太会,可以去搜索学习一下。这里主要介绍如何实现水波纹效果,其次是对自定义视图中常用的问题总结一下。
自定义视图会涉及到构造函数,现在的自定义视图构造函数有4个,最后一个一般是API21+,所以平时不常用。对这个不了解的可以去看看Android View 四个构造函数详解。
总结一下:
一般第一个构造函数是指在代码中创建实例化调用;第二个构造函数是通过XML方式建立控件视图,提供AttributeSet属性设置;第三个也是通过XML方式建立控件视图,提供AttributeSet属性设置,同时提供默认样式值defStyleAttr;第四个一样通过XML方式创建,除了提供跟第三个一样的参数外,新增defStyleRes。
View类的后两个构造函数都是与主题相关的。
它们的属性赋值优先级为:
XML直接定义 > XML中style引用 > defStyleAttr > defStyleRes > theme直接定义
了解了上面后,我们再来看看几个自定义视图中会常用到的函数:
onFinishInflate()
onAttachedToWindow()
onMeasure()
onSizeChanged ()
onLayout ()
onConfigurationChanged()
onDraw()
dispatchDraw ()
draw()
onTouchEvent()
onInterceptTouchEvent()
onFinishInflate()方法一般是在xml文件加载完成后调用这个方法;
onAttachedToWindow()方法是将视图依附到Window中;
onMeasure()方法是测量自定义视图的大小 ;
onSizeChanged()方法是自定义视图大小发生改变时调用;
onLayout()方法是将自定义视图放置到父容器的具体某个位置中;
onConfigurationChanged()方法是在当手机屏幕从横屏和竖屏相互转化时调用;
onDraw() 、dispatchDraw ()、draw()这三个方法,则是根据具体的情况来调用的;
自定义一个view时,重写onDraw()。
调用view.invalidate(),会导致draw流程重新执行。
view.postInvalidate(); //是在非UI线程上调用的自定义一个ViewGroup,重写onDraw()。
onDraw可能不会被调用,原因是需要先设置一个背景(颜色或图)。
表示这个group有东西需要绘制了,才会触发draw,之后是onDraw。
因此,一般直接重写dispatchDraw()来绘制viewGroupdispatchDraw会()对子视图进行分发绘制操作。
总结一下:
View组件的绘制会调用draw(Canvas canvas)方法,draw过程中主要是先画Drawable背景,对 drawable调用setBounds()然后是draw(Canvas c)方法。有点注意的是背景drawable的实际大小会影响view组件的大小,drawable的实际大小通过getIntrinsicWidth()和getIntrinsicHeight()获取,当背景比较大时view组件大小等于背景drawable的大小。
画完背景后,draw过程会调用onDraw(Canvas canvas)方法,然后就是dispatchDraw(Canvas canvas)方法, dispatchDraw()主要是分发给子组件进行绘制,我们通常定制组件的时候重写的是onDraw()方法。值得注意的是ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法, 而绕过了draw()方法,当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用。因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法,或者自定制一个Drawable,重写它的draw(Canvas c)和 getIntrinsicWidth()。
该总结来自网络,感谢分享~~~
最后是onTouchEvent()和onInterceptTouchEvent()方法,这两个方法也是我们经常会用到的。
onInterceptTouchEvent()方法定义在于ViewGroup中,默认返回值为false,表示不拦截TouchEvent()。onTouchEvent()方法定义在View中,当ViewGroup要调用onTouchEvent()时,调用super.onTouchEvent()方法。ViewGroup调用onTouchEvent()默认返回false,表示不消耗touch事件,View调用onTouchEvent()默认返回true,表示消耗了touch事件。
到这里我们把自定义视图会常遇到的方法都大致总结了一下。接下来再进行分析,因为有了这些方法,但我们还不知道它们调用的顺序,只有清楚了这些后才能做出更好的自定义视图。
首先建立自定义视图如下,继承View父类,然后打印出各种方法。
public class CustomView extends View {
public CustomView(Context context) {
super(context);
Log.i("anumbrella","constructor1");
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
Log.i("anumbrella","constructor2");
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.i("anumbrella","constructor3");
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
Log.i("anumbrella","constructor3");
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Log.i("anumbrella","onAttachedToWindow()");
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.i("anumbrella","onConfigurationChanged()");
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
Log.i("anumbrella","dispatchDraw()");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.i("anumbrella","onDraw()");
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
Log.i("anumbrella","draw()");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.i("anumbrella","onLayout()");
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.i("anumbrella","onMeasure()");
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.i("anumbrella", "onSizeChanged() " + " w = " + w + " h = " + h + " oldW = " + oldw + " oldH = " + oldw);
}
}
结果如下图:
我们可以看到自定义view执行的方法顺序为:constructor2->onAttachedToWindow()->onMeasure()->onSizeChanged()->onLayout()->onMeasure()->onLayout()->onDraw()->dispatchDraw()->draw()。
现在我们将上面的代码改为继承VIewGroup(如:RelativeLayout)的视图控件,再添加onTouchEvent()和onInterceptTouchEvent()方法,再运行。
如下:
为啥没有调用draw()方法?因为ViewGroup没有背景颜色,这就跟上面总结的一样的。我们加上背景颜色android:background=”@color/colorPrimary”。重新运行,结果可以看到调用了draw()方法:
然后我们在自定义的ViewGroup下面添加子视图,比如TextView重新运行,点击时就会调用onTouchEvent()方法。
这里只是对自定义视图会用到的方法进行了简单的介绍,更深入的了解在此不做过多介绍。
好了,接下来我们才要开始进入主题——自定义的水波纹实现效果。有了上面的知识,我相信对下面的代码理解就会容易多了。
先来看看效果:
具体的代码如下,我们接下来一步一步介绍:
public class RippleView extends RelativeLayout {
/**
* 水波纹的颜色
*/
private int rippleColor;
/**
* 水波纹扩散类型
*/
private Integer rippleType;
/**
* 放大持续时间
*/
private int zoomDuration;
/**
* 放大比例
*/
private float zoomScale;
/**
* 放大动画类
*/
private ScaleAnimation scaleAnimation;
/**
* 视图是否放大
*/
private Boolean hasToZoom;
/**
* 是否从视图中心开始动画
*/
private Boolean isCentered;
/**
* 帧速率
*/
private int frameRate = 10;
/**
* 水波纹持续时间
*/
private int rippleDuration = 400;
/**
* 水波纹透明度
*/
private int rippleAlpha = 90;
/**
* canvas画布执行Handler
*/
private Handler canvasHandler;
/**
* 水波纹画笔
*/
private Paint paint;
/**
* 水波纹扩散内边距
*/
private int ripplePadding;
/**
* 手势监听类
*/
private GestureDetector gestureDetector;
/**
* 水波纹动画是否开始
*/
private boolean animationRunning = false;
/**
* 时间统计
*/
private int timer = 0;
/**
* 时间间隔
*/
private int timerEmpty = 0;
/**
* 水波纹持续时间间隔
*/
private int durationEmpty = -1;
/**
* 最大圆半径
*/
private float radiusMax = 0;
/**
* 水波纹圆的坐标点
*/
private float x = -1;
private float y = -1;
private Bitmap originBitmap;
private OnRippleCompleteListener onCompletionListener;
/**
* 视图的宽和高
*/
private int WIDTH;
private int HEIGHT;
/**
* 定义水波纹类型
*/
public enum RippleType {
SIMPLE(0),
DOUBLE(1),
RECTANGLE(2);
int type;
RippleType(int type) {
this.type = type;
}
}
/**
* 水波纹更新波纹Runnable
*/
private final Runnable runnable = new Runnable() {
@Override
public void run() {
invalidate();
}
};
/**
* 定义回调函数,当水波纹效果完成时调用
*/
public interface OnRippleCompleteListener {
void onComplete(RippleView rippleView);
}
public RippleView(Context context) {
super(context);
}
public RippleView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public RippleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
/**
* 初始化方法
*
* @param context
* @param attrs
*/
private void init(Context context, AttributeSet attrs) {
if (isInEditMode()) {
return;
}
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RippleView);
rippleColor = typedArray.getColor(R.styleable.RippleView_rv_color, getResources().getColor(R.color.rippelColor));
rippleType = typedArray.getInt(R.styleable.RippleView_rv_type, 0);
hasToZoom = typedArray.getBoolean(R.styleable.RippleView_rv_zoom, false);
isCentered = typedArray.getBoolean(R.styleable.RippleView_rv_centered, false);
rippleDuration = typedArray.getInteger(R.styleable.RippleView_rv_rippleDuration, rippleDuration);
rippleAlpha = typedArray.getInteger(R.styleable.RippleView_rv_alpha, rippleAlpha);
ripplePadding = typedArray.getDimensionPixelSize(R.styleable.RippleView_rv_ripplePadding, 0);
canvasHandler = new Handler();
zoomScale = typedArray.getFloat(R.styleable.RippleView_rv_zoomScale, 1.03f);
zoomDuration = typedArray.getInt(R.styleable.RippleView_rv_zoomDuration, 200);
typedArray.recycle();
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(rippleColor);
paint.setAlpha(rippleAlpha);
//使onDraw方法可以调用,以便被我们重写
this.setWillNotDraw(false);
gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public void onLongPress(MotionEvent event) {
super.onLongPress(event);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return true;
}
});
//开启cache来绘制视图
this.setDrawingCacheEnabled(true);
this.setClickable(true);
}
/**
* 绘制水波纹
*
* @param canvas
*/
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (animationRunning) {
canvas.save();
if (rippleDuration <= timer * frameRate) {
animationRunning = false;
timer = 0;
durationEmpty = -1;
timerEmpty = 0;
//android 23 会自动调用canvas.restore();
if (Build.VERSION.SDK_INT != 23) {
canvas.restore();
}
invalidate();
if (onCompletionListener != null) {
onCompletionListener.onComplete(this);
}
return;
} else {
canvasHandler.postDelayed(runnable, frameRate);
}
if (timer == 0) {
canvas.save();
}
canvas.drawCircle(x, y, (radiusMax * (((float) timer * frameRate) / rippleDuration)), paint);
paint.setColor(Color.parseColor("#ffff4444"));
if (rippleType == 1 && originBitmap != null && (((float) timer * frameRate) / rippleDuration) > 0.4f) {
if (durationEmpty == -1) {
durationEmpty = rippleDuration - timer * frameRate;
}
timerEmpty++;
final Bitmap tmpBitmap = getCircleBitmap((int) ((radiusMax) * (((float) timerEmpty * frameRate) / (durationEmpty))));
canvas.drawBitmap(tmpBitmap, 0, 0, paint);
tmpBitmap.recycle();
}
paint.setColor(rippleColor);
if (rippleType == 1) {
if ((((float) timer * frameRate) / rippleDuration) > 0.6f) {
paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timerEmpty * frameRate) / (durationEmpty)))));
} else {
paint.setAlpha(rippleAlpha);
}
} else {
paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timer * frameRate) / rippleDuration))));
}
timer++;
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
WIDTH = w;
HEIGHT = h;
scaleAnimation = new ScaleAnimation(1.0f, zoomScale, 1.0f, zoomScale, w / 2, h / 2);
scaleAnimation.setDuration(zoomDuration);
scaleAnimation.setRepeatMode(Animation.REVERSE);
scaleAnimation.setRepeatCount(1);
}
/**
* 启动水波纹动画,通过MotionEvent事件
*
* @param event
*/
public void animateRipple(MotionEvent event) {
createAnimation(event.getX(), event.getY());
}
/**
* 启动水波纹动画,通过x,y坐标
*
* @param x
* @param y
*/
public void animateRipple(final float x, final float y) {
createAnimation(x, y);
}
private void createAnimation(final float x, final float y) {
if (this.isEnabled() && !animationRunning) {
if (hasToZoom) {
this.startAnimation(scaleAnimation);
}
radiusMax = Math.max(WIDTH, HEIGHT);
if (rippleType != 2) {
radiusMax /= 2;
}
radiusMax -= ripplePadding;
if (isCentered || rippleType == 1) {
this.x = getMeasuredWidth() / 2;
this.y = getMeasuredHeight() / 2;
} else {
this.x = x;
this.y = y;
}
animationRunning = true;
if (rippleType == 1 && originBitmap == null) {
originBitmap = getDrawingCache(true);
}
invalidate();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (gestureDetector.onTouchEvent(event)) {
animateRipple(event);
sendClickEvent(false);
}
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
this.onTouchEvent(event);
return super.onInterceptTouchEvent(event);
}
/**
* 发送一个点击事件,如果父视图是ListView实例
*
* @param isLongClick
*/
private void sendClickEvent(final Boolean isLongClick) {
if (getParent() instanceof AdapterView) {
final AdapterView adapterView = (AdapterView) getParent();
final int position = adapterView.getPositionForView(this);
final long id = adapterView.getItemIdAtPosition(position);
if (isLongClick) {
if (adapterView.getOnItemLongClickListener() != null) {
adapterView.getOnItemLongClickListener().onItemLongClick(adapterView, this, position, id);
}
} else {
if (adapterView.getOnItemClickListener() != null) {
adapterView.getOnItemClickListener().onItemClick(adapterView, this, position, id);
}
}
}
}
/**
* 设置水波纹的颜色
*
* @param rippleColor
*/
public void setRippleColor(int rippleColor) {
this.rippleColor = getResources().getColor(rippleColor);
}
public int getRippleColor() {
return rippleColor;
}
public RippleType getRippleType() {
return RippleType.values()[rippleType];
}
/**
* 设置水波纹动画类型,默认为RippleType.SIMPLE
*
* @param rippleType
*/
public void setRippleType(final RippleType rippleType) {
this.rippleType = rippleType.ordinal();
}
public Boolean isCentered() {
return isCentered;
}
/**
* 设置水波纹动画是否开始从父视图中心开始,默认为false
*
* @param isCentered
*/
public void setCentered(final Boolean isCentered) {
this.isCentered = isCentered;
}
public int getRipplePadding() {
return ripplePadding;
}
/**
* 设置水波纹内边距,默认为0dip
*
* @param ripplePadding
*/
public void setRipplePadding(int ripplePadding) {
this.ripplePadding = ripplePadding;
}
public Boolean isZooming() {
return hasToZoom;
}
/**
* 在水波纹结束后,是否有放大动画,默认为false
*
* @param hasToZoom
*/
public void setZooming(Boolean hasToZoom) {
this.hasToZoom = hasToZoom;
}
public float getZoomScale() {
return zoomScale;
}
/**
* 设置放大动画比例
*
* @param zoomScale
*/
public void setZoomScale(float zoomScale) {
this.zoomScale = zoomScale;
}
public int getZoomDuration() {
return zoomDuration;
}
/**
* 设置放大动画持续时间,默认为200ms
*
* @param zoomDuration
*/
public void setZoomDuration(int zoomDuration) {
this.zoomDuration = zoomDuration;
}
public int getRippleDuration() {
return rippleDuration;
}
/**
* 设置水波纹动画持续时间,默认为400ms
*
* @param rippleDuration
*/
public void setRippleDuration(int rippleDuration) {
this.rippleDuration = rippleDuration;
}
public int getFrameRate() {
return frameRate;
}
/**
* 设置水波纹动画的帧速率,默认为10
*
* @param frameRate
*/
public void setFrameRate(int frameRate) {
this.frameRate = frameRate;
}
public int getRippleAlpha() {
return rippleAlpha;
}
/**
* 设置水波纹动画的透明度,默认为90,取值为0到255之间
*
* @param rippleAlpha
*/
public void setRippleAlpha(int rippleAlpha) {
this.rippleAlpha = rippleAlpha;
}
public void setOnRippleCompleteListener(OnRippleCompleteListener listener) {
this.onCompletionListener = listener;
}
/**
* 绘制扩散背景范围视图bitmap
*
* @param radius
* @return
*/
private Bitmap getCircleBitmap(final int radius) {
final Bitmap output = Bitmap.createBitmap(originBitmap.getWidth(), originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(output);
final Paint paint = new Paint();
final Rect rect = new Rect((int) (x - radius), (int) (y - radius), (int) (x + radius), (int) (y + radius));
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
canvas.drawCircle(x, y, radius, paint);
//出来两图交叉情况
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(originBitmap, rect, rect, paint);
return output;
}
}
定义RippleView类继承RelativeLayout。为啥是RelativeLayout?觉得可能比LinearLayout耗性能,但是它的扩展性毕竟好很多。所以这点还是可以忽略的,其次通过构造函数,引入attr下的style属性。
attr下的属性为:
<!--自定义水波纹样式属性-->
<declare-styleable name="RippleView">
<!--定义透明度-->
<attr name="rv_alpha" format="integer" />
<!--画面frame速率(帧速率)-->
<attr name="rv_framerate" format="integer" />
<!--水波纹持续时间-->
<attr name="rv_rippleDuration" format="integer" />
<!--视图放大持续时间-->
<attr name="rv_zoomDuration" format="integer" />
<!--扩散水波纹颜色-->
<attr name="rv_color" format="color" />
<!--是否从中心扩散-->
<attr name="rv_centered" format="boolean" />
<!--水波纹样式-->
<attr name="rv_type" format="enum">
<enum name="simpleRipple" value="0" />
<enum name="doubleRipple" value="1" />
<enum name="rectangle" value="2" />
</attr>
<!--水波纹扩散内边距-->
<attr name="rv_ripplePadding" format="dimension" />
<!--水波纹是否放大-->
<attr name="rv_zoom" format="boolean" />
<!--放大比例-->
<attr name="rv_zoomScale" format="float" />
</declare-styleable>
有了这些后,我们就在init()函数中获取定义的属性,如果没有获取到
XML中定义的属性就设置为默认的值。然后调用onMeasure()来获取背
景视图的大小,再调用draw()方法去绘制。
在draw()方法中,一开始animationRunning是false,所以不会执行任何操作。
当我们点击视图时,这个onTouchEvent()方法就会调用,获取具体的x,y坐标,然后对radiusMax、animationRunning变量进行设置。
这个时候animationRunning为ture。最后调用invalidate(),重新draw()开始绘制。
在draw()中判断时间是否结束了,没有结束就通过canvasHandler来不停更新视图,调用draw()重新绘制视图。同时每次timer都会进行增加1,然后我们就通过timer的改变来实现半径大小的变化。每次绘制圆就形成了水波纹。
当要实现不同效果的水波纹时,即rippleType的值不一样时。就可以通过绘制不同的背景效果,改变透明度来实现。
好了,结束了。这就是水波纹自定义视图的大致实现,水波纹演示代码。