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()这三个方法,则是根据具体的情况来调用的;

  1. 自定义一个view时,重写onDraw()。
    调用view.invalidate(),会导致draw流程重新执行。
    view.postInvalidate(); //是在非UI线程上调用的

  2. 自定义一个ViewGroup,重写onDraw()。
    onDraw可能不会被调用,原因是需要先设置一个背景(颜色或图)。
    表示这个group有东西需要绘制了,才会触发draw,之后是onDraw。
    因此,一般直接重写dispatchDraw()来绘制viewGroup

  3. dispatchDraw会()对子视图进行分发绘制操作。

总结一下:
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()方法,再运行。
如下:
结果2

为啥没有调用draw()方法?因为ViewGroup没有背景颜色,这就跟上面总结的一样的。我们加上背景颜色android:background=”@color/colorPrimary”。重新运行,结果可以看到调用了draw()方法:

结果3

然后我们在自定义的ViewGroup下面添加子视图,比如TextView重新运行,点击时就会调用onTouchEvent()方法。

这里只是对自定义视图会用到的方法进行了简单的介绍,更深入的了解在此不做过多介绍。

好了,接下来我们才要开始进入主题——自定义的水波纹实现效果。有了上面的知识,我相信对下面的代码理解就会容易多了。

先来看看效果:
gif

具体的代码如下,我们接下来一步一步介绍:

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的值不一样时。就可以通过绘制不同的背景效果,改变透明度来实现。

好了,结束了。这就是水波纹自定义视图的大致实现,水波纹演示代码

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值