代码改变世界

[转]Android自定义控件系列五:自定义绚丽水波纹效果

2015-09-25 17:29  一切尽在掌握  阅读(311)  评论(0编辑  收藏  举报

出处:http://www.2cto.com/kf/201411/353169.html

今天我们来利用Android自定义控件实现一个比较有趣的效果:滑动水波纹。先来看看最终效果图:

图一

 

效果还是很炫的;饭要一口口吃,路要一步步走,这里我们将整个过程分成几步来实现

 

一、实现单击出现水波纹单圈效果:

图二

 

照例来说,还是一个自定义控件,这里我们直接让这个控件撑满整个屏幕(对自定义控件不熟悉的可以参看我之前的一篇文章:Android自定义控件系列二:自定义开关按钮(一))。观察这个效果,发现应该需要重写onTouchEvent和onDraw方法,通过在onTouchEvent中获取触摸的坐标,然后以这个坐标值为圆心来绘制我们需要的图形,这个绘制过程就是调用的onDraw方法。

 

1、新建一个工程,定义一个WaterWave的类,继承自View,作为一个自定义控件;在清单文件中将这个自定义控件写出来,直接填满父窗体。

 

2、在WaterWave类中,实现它的两参构造函数:

package com.example.waterwavedemo.ui;
 
 
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
 
 
public class WaterWave extends View {
    ...
    /*
     * 1、两参构造函数
     */
    public WaterWave(Context context, AttributeSet attrs) {
        super(context, attrs);
        alpha = 0;
        radius = 0;
        initPaint();
    }
    ...
}

 

 

3、要使用自定义控件,那么一般都需要指定它的大小,这里我们由于只需要其填满窗体,所以使用默认的onMeasure方法即可:

/**
     * onMeasure方法,确定控件大小,这里使用默认的
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // TODO Auto-generated method stub
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

 

4、将这个自定义图形画出来,重写onDraw方法,在这里由于我们需要画一个圈,所以这样写:

@Override
/**
 * 画出需要的图形的方法,这个方法比较关键
 */
protected void onDraw(Canvas canvas) {
    canvas.drawCircle(xDown, yDown, radius, paint);
 
}

 

其中的参数xDown和yDown是成员变量,代表按下时的x和y坐标,这个坐标所对应的点就是要绘制的圆环的圆心;radius参数也是成员变量,代表要绘制的圆环的半径;

 

看到这里还需要一个paint,是Paint类型的画笔对象,这里先将其定义成一个成员变量,由于onDraw方法在第一次自定义控件显示的时候就 会被调用,所以这个paint需要我们在两参的构造函数中就进行初始化,否则会报出空指针异常;那么我们这里另外写一个initPaint()方法来初始 化我们的paint:

 

/**
 * 初始化paint
 */
private void initPaint() {
    /*
     * 新建一个画笔
     */
    paint = new Paint();
 
    paint.setAntiAlias(true);
    paint.setStrokeWidth(width);
 
    // 设置是环形方式绘制
    paint.setStyle(Paint.Style.STROKE);
 
    System.out.println(alpha= + alpha);
    paint.setAlpha(alpha);
    System.out.println(得到的透明度: + paint.getAlpha());
 
    paint.setColor(Color.RED);
}

 

5、触摸定时刷新

在onDraw方法之后,我们已经可以画出这个圆环了,但是实际问题是,我们想要实现点击的时候才在点击的位置来画一个圆环,那么我们肯定需要获得 点击的时候的坐标xDown和yDown,所以肯定需要重写onTouchEvent方法,另外我们需要在按下的时候,让透明度是最不透明 (alpha=255),在绘制的过程中,让圆环的半径(radius)不断扩大,同时让透明度不断减小,直至完全透明(alpha=0),这个不断变化 的过程又需要每隔一段时间重新刷新状态和重新绘制图形,所以我们这里使用handler来处理:

@Override
    /**
     * 触摸事件的方法
     */
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
 
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            radius = 0;
            alpha = MAX_ALPHA;
            width = radius / 4;
            xDown = (int) event.getX();
            yDown = (int) event.getY();
 
            handler.sendEmptyMessage(0);
 
            break;
        case MotionEvent.ACTION_MOVE:
 
            break;
        case MotionEvent.ACTION_UP:
 
            break;
 
        default:
            break;
        }
 
        return true;
    }

 

可以看到,我们这里先只实现了ACTION_DOWN里面的逻辑,在每一个按下的时候将半径radius设置为0,透明度alpha设置为完全不透明,而 宽度也为0,并且获取按下的x和y坐标,之后就使用handler发送了一个空消息,让handler去实现定时刷新状态和绘制图形的工作,我们想让圆环 的透明度alpha捡到0的时候就不再继续定时自动刷新了,否则在每一次handleMessage的时候都先刷新状态值,然后绘制图形:

 

private Handler handler = new Handler() {
 
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
        case 0:
            flushState();
 
            invalidate();
 
            if (alpha != 0) {
                // 如果透明度没有到0,则继续刷新,否則停止刷新
                handler.sendEmptyMessageDelayed(0, 50);
            }
 
            break;
 
        default:
            break;
        }
    }
 
    /**
     * 刷新状态
     */
    private void flushState() {
        radius += 5;
        alpha -= 10;
        if (alpha < 0) {
            alpha = 0;
        }
        // System.out.println(alpha= + alpha);
        width = radius / 4;
 
        paint.setAlpha(alpha);
        paint.setStrokeWidth(width);
    }
 
};

 

我们可以看到,在handler中,我们重写了handleMessage方法,在msg.what=0的时候,我们调用flushState() 方法来刷新状态,和invalidate()方法来绘制图形,,然后使用handler.sendEmptyMessageDelayed(0, 50);来每隔50毫秒重复一次上面的工作;其中invalidate()是Android提供的,而flushState()则需要我们自己来实现;

按照我们的需求,每一次状态的刷新工作flushState(),我们需要做如下几件事:

(1)让半径增加

(2)让透明度减少,并设置给paint;

(3)环形的宽度增加,并设置给paint

(4)对于透明度而言,最大值是255,但是这里如果让透明度减少到0以下,比如说-1,那么实际上alpha的值不会是-1,而是255+(-1)=254,所以我们还需要加一个判断条件,防止alpha<0

 

/**
         * 刷新状态
         */
        private void flushState() {
            radius += 5;
            alpha -= 10;
            if (alpha < 0) {
                alpha = 0;
            }
            // System.out.println(alpha= + alpha);
            width = radius / 4;
 
            paint.setAlpha(alpha);
            paint.setStrokeWidth(width);
        }

 

6、在两参的构造函数中添加一些初始化工作:

 

public WaterWave(Context context, AttributeSet attrs) {
        super(context, attrs);
        alpha = 0;
        radius = 0;
        initPaint();
    }

 

至此,我们的第一步就基本完成了

 

二、实现多次点击圆环同时存在,同时刷新效果:

 

从面图二中,我们不难发现,不论如何点击,屏幕上都只会同时存在一个圆圈的效果,这是因为我们每次点击的时候,都重新设置了圆心,而且所有圆形的参 数都是成员变量,都是共享的;不仅如此,如果在上一个圆圈没有消失的时候,就再次点击,会让新出现的圆圈变大的速度大大增加,这是因为使用 handler.sendEmptyMessageDelayed(0,50)方法的原因,第二次点击时会重复触发这个方法,使得前后两次点击的 handler.sendEmptyMessageDelayed()重叠生效,让实际间隔远远小于50毫秒,所以刷新速度快了很多

那么我们现在就要解决上面两个小问题,实现如下图的效果:

解决这两个小问题的思路:

1、针对所有水波纹圆圈共享参数的问题:

方法就是新建一个内部类Wave,用于存放每个圆圈的参数,每一个圆圈都对应一个Wave对象,然后在onDraw方法里面,同时重绘所有的圆圈视图;那么这里就还需要一个List集合waveList,用于存放所有的wave对象,方便遍历。

 

2、针对handler.sendEmptyMessageDelayed方法在后续点击的时候不断被调用,导致刷新越来越快的问题。

这里可以设置一个成员变量 boolean isStart;来标志是不是第一次按下;因为我们在第一次按下的时候,肯定是希望开始定时刷新,调用 handler.sendEmptyMessageDelayed,让圆环的状态不断变化。但是对于之后的点击,我们其实只希望它立刻被刷新一次,并被加 入到waveList集合中,而并不需要发送一个handler的信息来调用handler.sendEmptyMessageDelayed。所以在一 开始的时候我们将其设置为true,而在第一次点击时候将其设置为false,那么在什么时候将其设置为false呢,这里牵涉到第三个问题:

 

3、对于waveList集合而言,如果一直点击往集合里面添加Wave对象,那么无疑会让这个集合越来越大,这个是我们不希望看到的。

我们希望在圆环的透明度值alpha变为0,也就是完全透明的时候,让其从waveList中remove掉,让其能被垃圾回收回收掉,这样如果点 击几个点之后停止,点都会自动消失(alpha值减到0),那么对应的Wave对象也会从waveList被移除,waveList的大小也会变成0,这 个时候我们就可以停止handler.sendEmptyMessageDelayed方法继续被调用,同时可以将isStart重新设为true。那么 isStart何时设为false呢?我们可以在flushState刷新状态的时候将其设为false,因为刷新状态的时候表明第一次点击已经按下了。 然后在onTouchEvent方法的ACTION_DWON条件下,如果isStart为true才发送handler的消息,这代表第一次点击,之后 再点击也不会发送而只是将wave对象添加到waveList中,因为第一次的时候调用flushState已经将isStart置为false了。

由于改动较大,代码如下:

 

package com.example.waterwavedemo.ui;
 
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
 
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
 
public class WaterWave extends View {
 
    /**
     * 波形的List
     */
    private List<wave> waveList;
 
    /**
     * 最大的不透明度,完全不透明
     */
    private static final int MAX_ALPHA = 255;
 
    protected static final int FLUSH_ALL = -1;
 
    private boolean isStart = true;
 
    // /**
    // * 按下的时候x坐标
    // */
    // private int xDown;
    // /**
    // * 按下的时候y的坐标
    // */
    // private int yDown;
    // /**
    // * 用来表示圆环的半径
    // */
    // private float radius;
    // private int alpha;
 
    /*
     * 1、两参构造函数
     */
    public WaterWave(Context context, AttributeSet attrs) {
        super(context, attrs);
        waveList = Collections.synchronizedList(new ArrayList<wave>());
    }
 
    /**
     * onMeasure方法,确定控件大小,这里使用默认的
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
 
    @Override
    /**
     * 画出需要的图形的方法,这个方法比较关键
     */
    protected void onDraw(Canvas canvas) {
        // 重绘所有圆环
        for (int i = 0; i < waveList.size(); i++) {
            Wave wave = waveList.get(i);
            canvas.drawCircle(wave.xDown, wave.yDown, wave.radius, wave.paint);
        }
 
    }
 
    /**
     * 初始化paint
     */
    private Paint initPaint(int alpha, float width) {
        /*
         * 新建一个画笔
         */
        Paint paint = new Paint();
 
        paint.setAntiAlias(true);
        paint.setStrokeWidth(width);
 
        // 设置是环形方式绘制
        paint.setStyle(Paint.Style.STROKE);
 
        // System.out.println(alpha= + alpha);
        paint.setAlpha(alpha);
        // System.out.println(得到的透明度: + paint.getAlpha());
 
        paint.setColor(Color.RED);
        return paint;
    }
 
    private Handler handler = new Handler() {
 
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
            case 0:
                flushState();
 
                invalidate();
 
                if (waveList != null && waveList.size() > 0) {
                    handler.sendEmptyMessageDelayed(0, 50);
                }
 
                break;
 
            default:
                break;
            }
        }
 
    };
 
    /**
     * 刷新状态
     */
    private void flushState() {
        for (int i = 0; i < waveList.size(); i++) {
            Wave wave = waveList.get(i);
            if (isStart == false && wave.alpha == 0) {
                waveList.remove(i);
                wave.paint = null;
                wave = null;
                continue;
            } else if (isStart == true) {
                isStart = false;
            }
            wave.radius += 5;
            wave.alpha -= 10;
            if (wave.alpha < 0) {
                wave.alpha = 0;
            }
            wave.width = wave.radius / 4;
            wave.paint.setAlpha(wave.alpha);
            wave.paint.setStrokeWidth(wave.width);
        }
 
    }
 
    // private Paint paint;
    // private float width;
 
    @Override
    /**
     * 触摸事件的方法
     */
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
 
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Wave wave = new Wave();
            wave.radius = 0;
            wave.alpha = MAX_ALPHA;
            wave.width = wave.radius / 4;
            wave.xDown = (int) event.getX();
            wave.yDown = (int) event.getY();
            wave.paint = initPaint(wave.alpha, wave.width);
            if (waveList.size() == 0) {
                isStart = true;
            }
            System.out.println(isStart= + isStart);
            waveList.add(wave);
            // 点击之后刷洗一次图形
            // invalidate();
            if (isStart) {
                handler.sendEmptyMessage(0);
            }
            break;
        case MotionEvent.ACTION_MOVE:
 
            break;
        case MotionEvent.ACTION_UP:
 
            break;
 
        default:
            break;
        }
 
        return true;
    }
 
    private class Wave {
        int waveX;
        int waveY;
        /**
         * 用来表示圆环的半径
         */
        float radius;
        Paint paint;
        /**
         * 按下的时候x坐标
         */
        int xDown;
        /**
         * 按下的时候y的坐标
         */
        int yDown;
        float width;
        int alpha;
    }
 
}
</wave></wave>

 

三、实现完全效果(点击和移动,颜色随机,圆圈大小变化速度)

效果图就是跟图一的一样了,主要做几个小地方:

1、让onTouchEvent里面的ACTION_DOWN和ACTION_MOVE响应同样的事件,实际上就是去掉ACTION_DOWN的break;然后将处理代码写到随后的ACTION_MOVE中去即可

 

2、新建一个成员变量数组colors,里面放自己想要的颜色,然后在initPaint方法的设置color的时候,使用paint.setColor(colors[(int) (Math.random() * (colors.length - 1))]);

 

3、控制波形的变化趋势,这个看个人爱好,我是这样做的:在flushState中:

 

wave.radius += waveList.size() - i;
wave.width = (wave.radius / 3);
wave.paint.setStrokeWidth(wave.width);
 
// wave.alpha -= 10;
if (wave.alpha < 0) {
    wave.alpha = 0;
}
// wave.width = wave.radius / 4;
wave.paint.setAlpha(wave.alpha);

 

至此,就完成了自定义的水波纹效果了。存在的问题就是,如果在模拟器上,快速滑动,会有卡顿,在我的手机Nexus5上,还算流畅,应该跟内存无关,后续可能还会做一些优化。