Android中水波纹使用之自定义视图实现
在前面一篇博客中介绍了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" /> <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的值不一样时。就可以通过绘制不同的背景效果,改变透明度来实现。
好了,结束了。这就是水波纹自定义视图的大致实现,水波纹演示代码。
更多相关文章
- Android(安卓)之 Window、WindowManager 与窗口管理
- 范例解析:学习Android的IPC主板模式
- Android(安卓)文件保存getFilesDir()丶getCacheDir()、getExtern
- 范例解析:学习Android的IPC主板模式
- Android应用程序的Life Cycle
- Android(安卓)onTouchEvent, onClick及onLongClick的调用机制
- 当我们按下电源键,Android(安卓)究竟做了些什么?
- 浅谈Java中Collections.sort对List排序的两种方法
- Python list sort方法的具体使用