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

发布时间 - 2026-01-11 00:05:10    点击率:

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

先分析一下雨滴的实现:

  1. 每个雨滴其实就是一条线,通过 canvas.drawLine() 绘制
  2. 线(雨滴)的长度、宽度、下落速度、透明度以及位置都是在一定范围内随机生成
  3. 每 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 线程中的,绘制线程需要我们自己手动创建。

具体可看官方文档:https://developer.android.google.cn/reference/android/view/SurfaceView.html

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, attrs, defStyleAttr);
 }

 // SurfaceView 创建时调用(可见)
 @Override
 public void surfaceCreated(SurfaceHolder holder) {

 }

 // SurfaceView 销毁时调用(不可见)
 @Override
 public void surfaceDestroyed(SurfaceHolder holder) {

 }

 @Override
 public void surfaceChanged(SurfaceHolder holder, int format, int width, 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<RainHolder> 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, 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开发者们能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。


# android  # surfaceview  # 下雨动画效果  # android的surfaceview  # android使用surfaceview+MediaPlayer播放视频  # Android使用SurfaceView实现飘赞动画  # Android Surfaceview的绘制与应用  # Android截屏SurfaceView黑屏问题的解决办法  # Android利用SurfaceView实现简单计时器  # Android切换至SurfaceView时闪屏(黑屏闪一下)以及黑屏移动问题的解决方法  # Android SurfaceView基础用法详解  # 要想  # 重写  # 是在  # 回调  # 你可以  # 使其  # 来实现  # 自己的  # 的是  # 都是  # 实现了  # 都有  # 放在  # 过了  # 是有  # 都要  # 让我们  # 还会  # 是怎么  # 不多 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: 如何快速搭建高效服务器建站系统?  在线教育网站制作平台,山西立德教育官网?  Windows10如何更改计算机工作组_Win10系统属性修改Workgroup  如何在万网开始建站?分步指南解析  Laravel如何配置和使用缓存?(Redis代码示例)  Laravel如何使用Service Provider服务提供者_Laravel依赖注入与容器绑定【深度】  如何快速查询域名建站关键信息?  php8.4header发送头信息失败怎么办_php8.4header函数问题解决【解答】  如何用PHP快速搭建高效网站?分步指南  Laravel怎么清理缓存_Laravel optimize clear命令详解  悟空识字怎么关闭自动续费_悟空识字取消会员自动扣费步骤  JS中对数组元素进行增删改移的方法总结  Windows驱动无法加载错误解决方法_驱动签名验证失败处理步骤  php嵌入式断网后怎么恢复_php检测网络重连并恢复硬件控制【操作】  Python函数文档自动校验_规范解析【教程】  如何在沈阳梯子盘古建站优化SEO排名与功能模块?  Laravel怎么多语言本地化设置_Laravel语言包翻译与Locale动态切换【手册】  微信小程序 闭包写法详细介绍  如何在阿里云部署织梦网站?  Laravel如何将应用部署到生产服务器_Laravel生产环境部署流程  在线制作视频网站免费,都有哪些好的动漫网站?  Laravel如何使用Gate和Policy进行授权?(权限控制)  JavaScript模板引擎Template.js使用详解  零服务器AI建站解决方案:快速部署与云端平台低成本实践  用v-html解决Vue.js渲染中html标签不被解析的问题  EditPlus 正则表达式 实战(3)  🚀拖拽式CMS建站能否实现高效与个性化并存?  JavaScript如何实现音频处理_Web Audio API如何工作?  新三国志曹操传主线渭水交兵攻略  如何快速查询网站的真实建站时间?  使用豆包 AI 辅助进行简单网页 HTML 结构设计  Laravel如何优雅地处理服务层_在Laravel中使用Service层和Repository层  Laravel Debugbar怎么安装_Laravel调试工具栏配置指南  EditPlus中的正则表达式 实战(2)  Android 常见的图片加载框架详细介绍  如何为不同团队 ID 动态生成多个非值班状态按钮  HTML 中动态设置元素 name 属性的正确语法详解  微信h5制作网站有哪些,免费微信H5页面制作工具?  Zeus浏览器网页版官网入口 宙斯浏览器官网在线通道  品牌网站制作公司有哪些,买正品品牌一般去哪个网站买?  Swift中swift中的switch 语句  WEB开发之注册页面验证码倒计时代码的实现  Laravel如何使用模型观察者?(Observer代码示例)  如何基于云服务器快速搭建个人网站?  活动邀请函制作网站有哪些,活动邀请函文案?  Laravel如何记录自定义日志?(Log频道配置)  Android仿QQ列表左滑删除操作  香港代理服务器配置指南:高匿IP选择、跨境加速与SEO优化技巧  laravel怎么使用数据库工厂(Factory)生成带有关联模型的数据_laravel Factory生成关联数据方法  JavaScript实现Fly Bird小游戏