这是效果图,长按监听的逻辑被封装在自定义SurfaceView的内部。在模拟器上显得稍微有点卡。
如有错误,还请指正
知识点
- PathMeasure类的简单使用
- Path类的贝塞尔曲线实现
以下代码:
自定义SurfaceView:HeartShapeLikeEffectSfcView
package com.example.doge_heartshapelikeeffect.view;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff.Mode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnTouchListener;;
/**
*
* @author fhbianling--- A little learning is a dangerous thing.
* @version 创建时间:2016年6月29日 下午7:25:54
* @mail fhbianling@163.com
*
*/
public class HeartShapeLikeEffectSfcView extends SurfaceView implements Runnable, Callback, OnTouchListener {
private SurfaceHolder mHolder;
private Paint mPaint;
private List<HeartShape> mHeartShapes;
private int viewWidth;
private int viewHeight;
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mHolder = this.getHolder();
mHolder.addCallback(this);
mHeartShapes = new ArrayList<HeartShape>();
// 使SurfaceView的背景色为透明的步骤1
setZOrderOnTop(true);
mHolder.setFormat(PixelFormat.TRANSLUCENT);
// 若不做上述两步处理,并且canvas.drawColor(Color.TRANSPARENT),
// 则SurfaceView的背景色为黑色,不能达到背景色为父容器背景色的效果
// 做处理后,若在XML中设置该View的背景色也会正常显示,不设置则默认为父容器背景色
}
public HeartShapeLikeEffectSfcView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public HeartShapeLikeEffectSfcView(Context context) {
super(context);
init();
}
public HeartShapeLikeEffectSfcView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private boolean isDestroy;
/**
* 当Surface被创建时既开启一个线程,这个线程在mHeartShapes的长度为0时等待,长度大于0时被唤醒
* 并在Surface被销毁时停止
*/
@Override
public void run() {
while (true && !isDestroy) {
// 下面这个代码块负责绘制
synchronized (mHolder) {
Canvas lockCanvas = mHolder.lockCanvas();
drawHeart(lockCanvas);
mHolder.unlockCanvasAndPost(lockCanvas);
}
// 下面这个代码块负责该线程的等待操作的判定
synchronized (this) {
if (mHeartShapes.size() == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 遍历绘制集合中移动的心形
* @param canvas
*/
private void drawHeart(Canvas canvas) {
// 使SurfaceView的背景色为透明的步骤2
canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
if (mHeartShapes.size() == 0) {
return;
}
// 如果集合mHeartShapes中的某个形状已经移动完毕,则将其移除集合
// 否则调用这个形状的move方法,这个move方法封装了具体的绘制逻辑
for (int i = 0; i < mHeartShapes.size(); i++) {
HeartShape heartShape = mHeartShapes.get(i);
if (heartShape.isFinsh) {
mHeartShapes.remove(heartShape);
continue;
} else {
heartShape.move(canvas);
}
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// 初始化
isDestroy = false;
viewWidth = getWidth();
viewHeight = getHeight();
//开启绘制线程
new Thread(this).start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// 停止绘制线程
isDestroy = true;
}
/**
* 为集合添加指定数量的心形
* @param num
*/
public void addHeartShape(int num) {
if (mHeartShapes == null || viewWidth == 0 || mHeartShapes.size() > 20 || num == 0) {
return;
}
for (int i = 0; i < num; i++) {
mHeartShapes.add(new HeartShape(viewWidth, viewHeight, mPaint));
}
// 当mHeartShapes的长度不为0时唤醒绘制线程
synchronized (this) {
this.notify();
}
}
/**
* 为自定义View绑定长按事件的控制器,长按事件的所有逻辑都封装在该类内部
*
* @param controller
*/
public void bindLongClickViewController(View controller) {
controller.setOnTouchListener(this);
}
private boolean isAdding;
private AddThread thread;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isAdding = true;
//ACTION_DOWN时开启一个添加线程
if(thread==null||!thread.isAlive()){
thread = new AddThread();
thread.start();
}
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
isAdding = false;
break;
}
return false;
}
/**
* 该线程在长按发生时每隔一定时间为心型集合添加一个心形
*
* @author Administrator
*
*/
private class AddThread extends Thread {
@Override
public void run() {
long orgTime = System.currentTimeMillis();
while (isAdding) {
// if内的控制语句用于判断长按事件
if (System.currentTimeMillis() - orgTime > 1000) {
addHeartShape(1);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
其中使用到的类HeartShape
package com.example.doge_heartshapelikeeffect.view;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
/**
* @author fhbianling--- A little learning is a dangerous thing.
* @version 创建时间:2016年7月5日 上午10:10:49
* @mail fhbianling@163.com
*
*/
public class HeartShape {
private int color;
private int red, green, blue;
private float sidesLength;
private Path mPath;
private PathMeasure mMeasure;
private float length;
private float distance;
private float[] pos = new float[2];
public boolean isFinsh;
private Paint mPaint;
/**
* 在该构造中为心形的随机大小,随机路径及其长度,随机颜色进行初始化
*
* @param viewWidth
* @param viewHeight
* @param paint
*/
public HeartShape(int viewWidth, int viewHeight, Paint paint) {
mPaint = paint;
// 基础长度取自自定义View宽高最小值/10
float baseLength = Math.min(viewWidth, viewHeight) / 10;
// sidesLength用于控制心形大小
sidesLength = (float) (Math.random() * 0.6 * baseLength + baseLength * 0.7);
// 为颜色的三原色赋值
red = (int) getRandomValue(256);
green = (int) getRandomValue(256);
blue = (int) getRandomValue(256);
//生成心形的随机移动路径
mPath = new Path();
//moveTo控制路径起点,为View的下边界中点处
mPath.moveTo(viewWidth / 2, viewHeight);
float controlX = getRandomValue(viewWidth);
float controlY = getRandomValue(viewHeight);
float endX = getRandomValue(viewWidth);
//endY的赋值是为了使心形的移动路径在Y轴上始终是向View上方的
float endY = (float) (controlY * Math.abs((Math.random() - 0.3)));
//quadTo方法和cubicTo方法都可以生成贝塞尔曲线,唯一的区别是cubicTo多一个控制点
//quadTo方法中前两个参数是控制点的x,y坐标,后两个点是贝塞尔曲线的结束点坐标
mPath.quadTo(controlX, controlY, endX, endY);
// 这个构造中boolean forceClosed用于设定是否闭合Path
mMeasure = new PathMeasure(mPath, false);
}
/**
* 生成一个0~orgValue范围内的浮点数,不能取到orgValue,但可以无限接近
* @param orgValue
* @return
*/
private float getRandomValue(float orgValue) {
return (float) (Math.random() * orgValue);
}
/**
* 传入中心坐标绘制对应位置的心形
* @param canvas
* @param centerX
* @param centerY
* @param color
*/
private void mDrawHeart(Canvas canvas, float centerX, float centerY, int color) {
if (canvas == null) {
return;
}
//x0,y0是经坐标转换后的坐标原点
float x0 = centerX - sidesLength / 2;
float y0 = centerY - sidesLength / 2;
mPaint.setColor(color);
//用一条贝塞尔曲线制作心形的右半边Path,其各参数可随意调整获得不同的心形
//没有采用对心形线方程拟合的方法,而是直接通过两个连续光滑曲线闭合形成心形
Path path = new Path();
path.moveTo(x0 + sidesLength / 2, y0 + sidesLength / 3);
path.cubicTo(x0 + sidesLength * 6 / 7, y0 - sidesLength / 20, x0 + sidesLength * 1.3f, y0 + sidesLength * 3 / 5,
x0 + sidesLength / 2, y0 + sidesLength * 19 / 20);
//另一条制作左半边Path
Path path2 = new Path();
path2.moveTo(x0 + sidesLength / 2, y0 + sidesLength / 3);
path2.cubicTo(x0 + sidesLength / 7, y0 - sidesLength / 20, x0 - sidesLength * 0.3f, y0 + sidesLength * 3 / 5,
x0 + sidesLength / 2, y0 + sidesLength * 19 / 20);
//两个Path合并并闭合,得到心形的完整path
path.addPath(path2);
path.close();
canvas.drawPath(path, mPaint);
}
private int alpha;
/**
* 当调用move方法时,封装了该心形的移动和绘制
* @param canvas
*/
public void move(Canvas canvas) {
if (length == 0) {
//PathMeasure可以测量Path类对应路径的长度,并获得Path上各点的坐标
length = mMeasure.getLength();
}
//使每次调用move方法时,心形移动移动路径的1/100长度
distance += length / 100;
//通过Color.argb(...)方法设置带透明度的颜色
alpha = (int) ((length - distance) * 255 / length);
if (distance >= length && length != 0) {
//当distance>length时移动完毕,这时的alpha会变成负数
isFinsh = true;
return;
}
color = Color.argb(alpha, red, green, blue);
//PathMeasure的.getPosTan(...)方法会对pos(一个长度为2的float数组)赋值,
//对应Path上从起点开始沿路径移动distance距离的点的坐标
//pos[0]:x坐标,pos[1]:y坐标
//第三个参数用于获取对该点在路径上的切线的正切值,当沿路径移动的图像还需要旋转时需要这个参数
//这里并不需要,直接填null
mMeasure.getPosTan(distance, pos, null);
mDrawHeart(canvas, pos[0], pos[1],color);
}
}
然后是XML和Activity,很简单,就多写了一个Button
Activity:
package com.example.doge_heartshapelikeeffect.aty;
import com.example.doge_heartshapelikeeffect.R;
import com.example.doge_heartshapelikeeffect.view.HeartShapeLikeEffectSfcView;
import android.app.Activity;
import android.os.Bundle;
public class MainActivity extends Activity {
private HeartShapeLikeEffectSfcView view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
view=(HeartShapeLikeEffectSfcView) findViewById(R.id.view);
//将Button绑定为自定义View的长按控制器
view.bindLongClickViewController(findViewById(R.id.test));
}
}
XML
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:orientation="vertical"
tools:context=".MainActivity" >
<com.example.doge_heartshapelikeeffect.view.HeartShapeLikeEffectSfcView
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="400dp"/>
<Button android:id="@+id/test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="test"/>
</LinearLayout>
参考