android下雨动画效果,Android利用SurfaceView实现下雨的天气动画效果

首先是最终实现的效果图:

15059017111.gif?201729103647

先分析一下雨滴的实现:

每个雨滴其实就是一条线,通过canvas.drawLine()绘制

线(雨滴)的长度、宽度、下落速度、透明度以及位置都是在一定范围内随机生成

每 draw 一次然后改变雨滴的位置然后重绘即可实现雨滴的下落效果

分析完了,那么可以直接写一个类直接继承 View ,然后重写 onDraw()吗?可以看到效果图中的雨滴的下落速度很快,那么意味着每一帧都要调用 onDraw()一次使其重新绘制一次,假如你的 onDraw() 方法里面的渲染代码稍微有点费时,而 View 的 onDraw()方法调用是在 UI 线程中,那么绘制出来的效果就不会那么流畅,甚至还会阻塞 UI 线程,所以为了更流畅的效果并且不阻塞 UI 线程,我们这里使用 SurfaceView 来实现。

初识 SurfaceView

SurfaceView 直接继承自 View,View 必须在 UI 线程中绘制,而 SurfaceView 不同于 View,它可以在非 UI 线程中绘制并显示在界面上,这意味着你可以自己新开一个线程,然后把绘制渲染的代码放在该线程中。

Surface 是 Z 轴排序的,SurfaceView 的 Z 轴位置小于它的宿主 Window,代表它总是在自己所在 Window 的后面,既然在后面,那么是怎么显示的呢?SurfaceView 在其 Window 中打出一个“孔”(其实就是在其宿主 Window 上设置了一块透明区域来使其能够显示),意味着他的兄弟节点的 View 会覆盖它,例如你可以在 SurfaceView 上方放置按钮,文本等控件。

要想访问下面的 Surface ,可以通过 Android 提供给我们的 SurfaceHolder 接口。可以调用 SurfaceView 的 getHolder()来获取。

SurfaceView 是有生命周期的,我们必须在它生命周期期间进行执行绘制代码,所以我们需要监听 SurfaceView 的状态(例如创建以及销毁),这里 Android 为我们提供了 SurfaceHolder.Callback这个接口来可以让我们方便的监听 SurfaceView 的状态。

那么下面看下 SurfaceHolder.Callback接口

public interface Callback {

// SurfaceView 创建时调用(SurfaceView的窗口可见时)

public void surfaceCreated(SurfaceHolder holder);

// SurfaceView 改变时调用

public void surfaceChanged(SurfaceHolder holder,int format,int width,int height);

// SurfaceView 销毁时调用(SurfaceView的窗口不可见时)

public void surfaceDestroyed(SurfaceHolder holder);

}

我们的绘制代码需要在 surfaceCreated 和 surfaceDestroyed 之间执行,否则无效,SurfaceHolder.Callback的回调方法是执行在 UI 线程中的,绘制线程需要我们自己手动创建。

View 和 SurfaceView 的使用场景

View 适合那些与用户交互并且渲染时间不是很长的控件,因为 View 的绘制和用户交互都处在 UI 线程中。

SurfaceView 适合迅速的更新界面或者渲染时间比较长以至于影响到用户体验的场景。

使用 SurfaceView(实现)

这里我们和自定义 View 类似,写一个类 DynamicWeatherView 继承自 SurfaceView,然后为了监听 SurfaceView 的状态,所以我们还需要实现 SurfaceHolder.Callback接口来监听 SurfaceView 的状态,接口的回调具体时机上面也已经介绍过了。

public class DynamicWeatherView extends SurfaceView implements SurfaceHolder.Callback{

public DynamicWeatherView(Context context) {

this(context,null);

}

public DynamicWeatherView(Context context,AttributeSet attrs) {

this(context,attrs,0);

}

public DynamicWeatherView(Context context,AttributeSet attrs,int defStyleAttr) {

super(context,defStyleAttr);

}

// SurfaceView 创建时调用(可见)

@Override

public void surfaceCreated(SurfaceHolder holder) {

}

// SurfaceView 销毁时调用(不可见)

@Override

public void surfaceDestroyed(SurfaceHolder holder) {

}

@Override

public void surfaceChanged(SurfaceHolder holder,int height) {

}

}

上面也提到了,控制 Surface 我们需要 SurfaceHolder 对象,调用 SurfaceView 的getHolder()即可获得,然后为这个 SurfaceHolder 添加一个 SurfaceHolder.Callback回调,这里就是 DynamicWeatherView 当前对象

private SurfaceHolder mHolder;

mHolder = getHolder();

mHolder.addCallback(this);

mHolder.setFormat(PixelFormat.TRANSPARENT);

然后实现我们的绘制线程:

private class DrawThread extends Thread {

// 用来停止线程的标记

private boolean isRunning = false;

public void setRunning(boolean running) {

isRunning = running;

}

@Override

public void run() {

Canvas canvas;

// 无限循环绘制

while (isRunning) {

if (mType != null && mViewWidth != 0 && mViewHeight != 0) {

canvas = mHolder.lockCanvas();

if (canvas != null) {

mType.onDraw(canvas);

if (isRunning) {

mHolder.unlockCanvasAndPost(canvas);

} else {

// 停止线程

break;

}

SystemClock.sleep(1);

}

}

}

}

}

从上面的代码可以看出 SurfaceView 的更新流程具体为:

// 锁定画布并获得 canvas

canvas = mHolder.lockCanvas();

// 在 canvas 上进行绘制

mType.onDraw(canvas);

// 解除锁定并提交更改

mHolder.unlockCanvasAndPost(canvas);

绘制线程代码量不多,因为具体的绘制代码在 mType.onDraw(canvas)中,mType 是我们自己定义的一个接口,代表一种天气类型:

public interface WeatherType {

void onDraw(Canvas canvas);

void onSizeChanged(Context context,int w,int h);

}

这样要想实现不同的天气类型,只要实现这个接口重写 onDraw 和 onSizeChanged 方法即可,这里我们实现的是下雨的效果,所以实现了一个 RainTypeImpl 类:

public class RainTypeImpl extends BaseType {

// 背景

private Drawable mBackground;

// 雨滴集合

private ArrayList mRains;

// 画笔

private Paint mPaint;

public RainTypeImpl(Context context,DynamicWeatherView dynamicWeatherView) {

super(context,dynamicWeatherView);

init();

}

private void init() {

mPaint = new Paint();

mPaint.setAntiAlias(true);

mPaint.setColor(Color.WHITE);

// 这里雨滴的宽度统一为3

mPaint.setStrokeWidth(3);

mRains = new ArrayList<>();

}

@Override

public void generate() {

mBackground = getContext().getResources().getDrawable(R.drawable.rain_sky_night);

mBackground.setBounds(0,getWidth(),getHeight());

for (int i = 0; i < 60; i++) {

RainHolder rain = new RainHolder(

getRandom(1,getWidth()),getRandom(1,getHeight()),getRandom(dp2px(9),dp2px(15)),getRandom(dp2px(5),dp2px(9)),getRandom(20,100)

);

mRains.add(rain);

}

}

private RainHolder r;

@Override

public void onDraw(Canvas canvas) {

clearCanvas(canvas);

// 画背景

mBackground.draw(canvas);

// 画出集合中的雨点

for (int i = 0; i < mRains.size(); i++) {

r = mRains.get(i);

mPaint.setAlpha(r.a);

canvas.drawLine(r.x,r.y,r.x,r.y + r.l,mPaint);

}

// 将集合中的点按自己的速度偏移

for (int i = 0; i < mRains.size(); i++) {

r = mRains.get(i);

r.y += r.s;

if (r.y > getHeight()) {

r.y = -r.l;

}

}

}

private class RainHolder {

/**

* 雨点 x 轴坐标

*/

int x;

/**

* 雨点 y 轴坐标

*/

int y;

/**

* 雨点长度

*/

int l;

/**

* 雨点移动速度

*/

int s;

/**

* 雨点透明度

*/

int a;

public RainHolder(int x,int y,int l,int s,int a) {

this.x = x;

this.y = y;

this.l = l;

this.s = s;

this.a = a;

}

}

}

代码不难,基本都有注释,RainHolder 对象代表一个雨滴,每绘制一次然后改变雨滴的位置,然后准备下一次绘制,来实现雨滴的移动。

BaseType 类是我们的一个抽象基类,实现了 DynamicWeatherView.WeatherType接口,内部有一些公共方法,具体可以看 Demo 中的代码。

最后我们的 Activity 代码:

public class MainActivity extends Activity {

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

DynamicWeatherView mDynamicWeatherView = (DynamicWeatherView) findViewById(R.id.dynamic_weather_view);

mDynamicWeatherView.setType(new RainTypeImpl(this,mDynamicWeatherView));

}

}

今后要想实现不同的天气类型,只需要继承 BaseType 类重写相关方法即可。

源码下载:点击这里

总结

以上就是这篇文章的全部内容了,希望本文的内容对各位Android开发者们能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对编程小技巧的支持。

总结

如果觉得编程之家网站内容还不错,欢迎将编程之家网站推荐给程序员好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。

小编个人微信号 jb51ccc

喜欢与人分享编程技术与工作经验,欢迎加入编程之家官方交流群!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
效果开始前先做个热身( ˘•灬•˘ )自己实现比较容易,但是到了要出博客整理思路,总结要点的时候就挠头,不知云所以,所以最简单的还是 Read the fucking source code如果对安卓UI有兴趣的朋友可以加我好友互相探讨,这里有很多自定义view可以参考思路思路比较简单,整个view无非两样东西云滴这里又包含两部分动画,一部分是云的左右移动动画,一部分是滴移动动画 那我们这里可以自定义一些属性,如果对自定义属性还不太了解的同学,搜下百度哈<resources>     <declare-styleable name="RainyView">         <!--滴的颜色-->         <attr name="raindrop_color" format="color"></attr>         <!--左边云的颜色-->         <attr name="left_cloud_color" format="color"></attr>         <!--右边云的颜色-->         <attr name="right_cloud_color" format="color"></attr>         <!-可同时存在的滴的最大数量-->         <attr name="raindrop_max_number" format="integer"></attr>         <!--每个滴之间创建的时间间隔-->         <attr name="raindrop_creation_interval" format="integer"></attr>         <!--每个滴的最小长度-->         <attr name="raindrop_min_length" format="integer"></attr>         <!--每个滴的最大长度-->         <attr name="raindrop_max_length" format="integer"></attr>         <!--滴的大小-->         <attr name="raindrop_size" format="integer"></attr>         <!--滴的最小移动速度-->         <attr name="raindrop_min_speed" format="float"></attr>         <!--滴的最大移动速度-->         <attr name="raindrop_max_speed" format="float"></attr>         <!--滴的斜率-->         <attr name="raindrop_slope" format="float"></attr>     </declare-styleable> </resources>画云云怎么画?云的形状不可胜举,我这里只实现了一种简单的形状:那我们如何通过画笔将其画出来:1.首先,我们先画底部,底部是一个圆角的矩形,通过下面方法绘制添加圆角矩形path.addRoundRect(RectF rect, float rx, float ry, Direction dir) 2.在该圆角的矩形的基础上,再画两个圆,左边的为小圆,右边的为大圆,这样就产生了一个最简单的云的图形, 在设置了以下代码之后paint.setStyle(Paint.Style.FILL);云的效果如下:我们把这个云作为左边的云,那么右边的云怎么画?很简单,因为我们这里用path来装载了这个云的路径,通过以下方法,mComputeMatrix.preTranslate(rightCloudTranslateX, -calculateRect.height() * (1 - CLOUD_SCALE_RATIO) / 2); mComputeMatrix.postScale(CLOUD_SCALE_RATIO, CLOUD_SCALE_RATIO, rightCloudCenterX, leftCloudEndY); mLeftCloudPath.transform(mComputeMatrix, mRightCloudPath);将这个云的path移动,缩小,并将其路径转换到mRightCloudPath即可在onDraw()的时候,调用以下方法就可以描绘路径了canvas.drawPath()接下来我们来实现云的动画,我们由上面已经了解到:/**  * Transform the points in this path by matrix, and write the answer  * into dst. If dst is null, then the the original path is modified.  *  * @param matrix The matrix to apply to the path  * @param dst    The transformed path is written here. If dst is null,  *               then the the original path is modified  */ public void transform(Matrix matrix, Path dst) {     long dstNative = 0;     if (dst != null) {         dst.isSimplePath = false;         dstNative = dst.mNativePath;     }     nTransform(mNativePath, matrix.native_instance, dstNative); }该方法可以将一个path进行matrix转换,即矩阵转换,因此我们可以通过方法matrix.postTranslate来实现平移动画,即创建一个循环动画,通过postTranslate来设置动画值就可以了,这里左边的云在右边的云之上,因此先画右边的云。mComputeMatrix.reset(); mComputeMatrix.postTranslate((mMaxTranslationX / 2) * mRightCloudAnimatorValue, 0); mRightCloudPath.transform(mComputeMatrix, mComputePath); canvas.drawPath(mComputePath, mRightCloudPaint); mComputeMatrix.reset(); mComputeMatrix.postTranslate(mMaxTranslationX * mLeftCloudAnimatorValue, 0); mLeftCloudPath.transform(mComputeMatrix, mComputePath); canvas.drawPath(mComputePath, mLeftCloudPaint);画滴首先我们要知道一点是,所有的滴都是随机产生的,而产生的值,可以根据上面的自定义属性指定,也可以使用自定义的值,我们先定义一个滴类private class RainDrop{     float speedX;  //滴x轴移动速度     float speedY;   //滴y轴移动速度     float xLength; //滴的x轴长度     float yLength; //滴的y轴长度     float x;        //滴的x轴坐标     float y;        //滴的y轴坐标     float slope; //滴的斜率 }关于上面参数,这里画张图来示例:关于斜率 我这里开放了一个设置斜率的接口,代表滴的一个倾斜度,可以看到下图的滴都是倾斜的,就是通过斜率来设置这个倾斜度 斜率:表示一条直线(或曲线的切线)关于(横)坐标轴倾斜程度的量。它通常用直线(或曲线的切线)与(横)坐标轴夹角的正切,或两点的纵坐标之差与横坐标之差的比来表示。该直线的斜率为k=(y1-y2)/(x1-x2)我这里使用了固定的斜率,使所有的滴方向一致,如果想将其改为随机值的同学,可以下载源码自行修改。在创建滴对象的时候,以下步骤使我们需要做的:斜率赋值(我这里是指定的,因此不用计算随机斜率)计算x轴、y轴移动速度随机值计算滴长度随机值(同时计算x轴,y轴长度值)计算x,y坐标随机值(为了营造滴更好的出场效果,这里设置了y轴的起点坐标为y-滴y轴长度)创建滴对象后,我们有了想要的参数,我们可以canvas.drawLine来画滴canvas.drawLine(rainDrop.x, rainDrop.y,             rainDrop.slope > 0 ? rainDrop.x   rainDrop.xLength : rainDrop.x - rainDrop.xLength,             rainDrop.y   rainDrop.yLength,             mRainPaint);这里需要注意以下,为什么canvas.drawLine中的stopX参数要设置为rainDrop.slope > 0 ? rainDrop.x   rainDrop.xLength : rainDrop.x - rainDrop.xLength这是因为,我们的滴是一直往下移动即y是增加的,我们上面知道斜率公式为:k=(y1-y2)/(x1-x2)即y1-y2肯定是大于0的,因此当斜率小于0的时候,滴是这样的,即x1-x2 < 0 当斜率大于0的时候,滴是这样的,即x1-x2 > 0 动画,由于每一个滴对象参数已经定义,在进行动画的时候,只需要根据速度,设置x、y轴的下一个点的坐标就行了if (rainDrop.slope >= 0) {         rainDrop.x  = rainDrop.speedX;     }else{         rainDrop.x -= rainDrop.speedX;     } rainDrop.y  = rainDrop.speedY;优化我们知道,在频繁的创建滴的时候,如果每次都创建新对象的话, 可能会增加不必要的内存使用,而且很容易引起频繁的gc,甚至是内存抖动。因此这里我增加了一个回收功能//首先判断栈中是否存在回收的对象,若存在,则直接复用,若不存在,则创建一个新的对象 private RainDrop obtainRainDrop(){      if (mRecycler.isEmpty()){          return new RainDrop();      }      return mRecycler.pop();  } //回收到一个栈里面,若这个栈数量超过最大可显示数量,则pop private void recycle(RainDrop rainDrop){     if (rainDrop == null){         return;     }     if (mRecycler.size() >= mRainDropMaxNumber){         mRecycler.pop();     }     mRecycler.push(rainDrop); }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值