Flutter桌面小工具 -- 灵动岛【Windows+Android版本】

2023-11-14

通过此篇文章,你将了解到:

  1. Flutter动画实现灵动岛;
  2. Flutter如何开发一个置顶可自由拖拽的小工具;
  3. 分享一些关于灵动岛的想法。

⚠️本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前言

Flutter开发Windows应用已经见怪不怪了,我觉得可以尝试做一些小工具。恰逢近期最近苹果iphone 14系列推出“灵动岛”,这个酷炫的组件瞬间引起很多关注;而且看到一些前端博客用css实现灵动岛的效果;作为Flutter的忠实拥护者,前端能写的Flutter必须能写!

灵动岛效果实现

请添加图片描述

  • 小药丸放大
    小药丸放大的效果可以拆分为两步:横向放大+惯性缩放回弹。需要两个动画和控制器,当放大动画执行完毕的时候,执行缩放动画,从1.04到1.0。
// 初始化变量
late Animation<Size> _animation;
late AnimationController _animationController;
AnimationStatus status = AnimationStatus.forward;

late Animation<double> _scaleAnimation;
late AnimationController _scaleAnimationController;
void initState() {
  super.initState();

  _animationController = AnimationController(
    duration: const Duration(milliseconds: 500),
    vsync: this,
  );
  _animation = Tween<Size>(
    begin: Size(104.w, EnvConfig.relHeight),
    end: Size(168.w, EnvConfig.relHeight),
  ).animate(_animationController);

  _scaleAnimationController = AnimationController(
    duration: const Duration(milliseconds: 300),
    vsync: this,
  );
  _scaleAnimation = Tween<double>(
    begin: 1,
    end: 1,
  ).animate(_scaleAnimationController);

// 放大动画执行完毕,开始缩放动画,从1.04到1.0
_animationController.addStatusListener((status) {
  this.status = status;
  if (status == AnimationStatus.completed) {
    _scaleAnimation = Tween<double>(
      begin: count == 3 ? 1.04 : 1.06,
      end: 1,
    ).animate(_scaleAnimationController);
    _scaleAnimationController.forward(from: 0);
  }
});
}

布局上使用AnimatedBuilder监听animate值的变化,设置小药丸的宽高以达到放大和缩放效果。

AnimatedBuilder(
  animation: _scaleAnimation,
  builder: (context, _) => AnimatedBuilder(
    animation: _animation,
    builder: (context, _) => Container(
      width: _animation.value.width * _scaleAnimation.value,
      height: _animation.value.height * _scaleAnimation.value,
      clipBehavior: Clip.antiAliasWithSaveLayer,
      decoration: BoxDecoration(
        color: Colors.black,
        borderRadius: BorderRadius.all(
          Radius.circular(15.h),
        ),
      ),
    ),
  ),
),
  • i形分离效果 在小药丸后面要分离出一个小圆圈,从而实现i形的效果;这里也需要一个动画控制器,在布局上我们选择StackPositioned。分离过程就是小圆圈的右边距一直往负的方向放大,实现向右移出和向左缩回。
late Animation<double> _ballAnimation;
late AnimationController _ballAnimationController;
_ballAnimationController = AnimationController(
  duration: const Duration(milliseconds: 600),
  vsync: this,
);
_ballAnimation = Tween<double>(begin: 0, end: -EnvConfig.relHeight - 5)
    .chain(CurveTween(curve: Curves.easeInOut))
    .animate(_ballAnimationController);

// 当药丸缩回的过程中,执行分离动画
_animationController.addListener(() {
  if (count == 2 &&
      status == AnimationStatus.reverse &&
      _animationController.value > 0.25 &&
      _animationController.value < 0.3) {
    _ballAnimationController.forward(from: 0);
  }
});

上面是动画的过程,我们再看下布局的代码:

AnimatedBuilder(
  animation: _ballAnimation,
  builder: (context, _) => Stack(clipBehavior: Clip.none, children: [
    AnimatedBuilder(
      // .... 小药丸 ....
    ),
    Positioned(
      top: 0,
      right: _ballAnimation.value,
      child: Container(
        width: EnvConfig.relHeight,
        height: EnvConfig.relHeight,
        decoration: const BoxDecoration(
          shape: BoxShape.circle,
          color: Colors.black,
        ),
      ),
    ),
  ]),
),

动画其实非常简单,也没啥好讲的,重点在分享如何作为一个小工具。具体源码见文末仓库。

将应用配置为小工具【Windows端】

这里的前提是基于上一篇文章:做好屏幕的适配。
在windows上,小工具就是一个普通应用【这跟Android window_manager的机制是不一样的】。不过我们需要把宽高、位置设置好;同时还需要保证小工具置顶、没有状态栏图标
这里我们依然用到了window_manager的插件,每个步骤都有对应注释。

static late double relHeight;

static initWindow(List<String> args, {Size? screenSize}) async {
  // 注释:获取屏幕真实大小
  Display primaryDisplay = await screenRetriever.getPrimaryDisplay();
  relHeight = primaryDisplay.size.height * 0.04;
  double relWidth = relHeight * 8;
  final displaySize = Size(relWidth, relHeight * 1.06);
  await setSingleInstance(args);
  WindowManager w = WindowManager.instance;
  await w.ensureInitialized();
  WindowOptions windowOptions = WindowOptions(
    size: displaySize,
    minimumSize: displaySize,
    alwaysOnTop: true, // 注释:设置置顶
    titleBarStyle: TitleBarStyle.hidden, // 注释:去除窗口标题栏
    skipTaskbar: true // 注释:去除状态栏图标
  );
  w.waitUntilReadyToShow(windowOptions, () async {
    double w1 = (primaryDisplay.size.width - relWidth) / 2;
    await w.setBackgroundColor(Colors.transparent);
    await w.setPosition(Offset(w1, 10)); // 注释:设置居中
    await w.show();
    await w.focus();
    await w.setAsFrameless();
  });
}

这样我们就可以得到一个very good的小组件啦!

将应用配置为小工具【Android端】

Android小组件与Windows可是大有不同。由于Google基于安全的限制,Android应用必须是全屏且不允许穿透点击,因此Android的小组件一般都是依附于悬浮窗来开发的,即windows_manager
Flutter只是一个UI框架,自然也不能脱离Android本身的机制,因此我们需要在原生层创建一个悬浮窗,然后创建一个Flutter engine来吸附Flutter的UI。

  • 创建后台服务
<!-- 权限配置 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE " />

<service
    android:name=".WindowsService"
    android:enabled="true"
    android:exported="true">
</service>
  • 创建一个悬浮窗,实现步骤注意看其中的注释
package com.karl.open.desktop_app

import android.annotation.SuppressLint
import android.app.Service
import android.content.Intent
import android.graphics.PixelFormat
import android.os.IBinder
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
import com.karl.open.desktop_app.utils.Utils
import io.flutter.embedding.android.FlutterSurfaceView
import io.flutter.embedding.android.FlutterView
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineGroup
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.view.FlutterMain.findAppBundlePath

class WindowsService : Service() {
    // Flutter引擎组,可以自动管理引擎的生命周期
    private lateinit var engineGroup: FlutterEngineGroup

    private lateinit var engine: FlutterEngine

    private lateinit var flutterView: FlutterView
    private lateinit var windowManager: WindowManager

    private val metrics = DisplayMetrics()
    private lateinit var inflater: LayoutInflater

    @SuppressLint("InflateParams")
    private lateinit var rootView: ViewGroup

    private lateinit var layoutParams: WindowManager.LayoutParams

    override fun onCreate() {
        super.onCreate()
        layoutParams = WindowManager.LayoutParams(
            Utils.dip2px(this, 168.toFloat()),
            Utils.dip2px(this, 30.toFloat()),
            WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            PixelFormat.TRANSLUCENT
        )

        // 初始化变量
        windowManager = this.getSystemService(Service.WINDOW_SERVICE) as WindowManager
        inflater =
            this.getSystemService(Service.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        rootView = inflater.inflate(R.layout.floating, null, false) as ViewGroup
        engineGroup = FlutterEngineGroup(this)

        // 创建Flutter Engine
        val dartEntrypoint = DartExecutor.DartEntrypoint(findAppBundlePath(), "main")
        val option =
            FlutterEngineGroup.Options(this).setDartEntrypoint(dartEntrypoint)
        engine = engineGroup.createAndRunEngine(option)

        // 设置悬浮窗的位置
        @Suppress("Deprecation")
        windowManager.defaultDisplay.getMetrics(metrics)
        setPosition()
        @Suppress("ClickableViewAccessibility")
        rootView.setOnTouchListener { _, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    layoutParams.flags =
                        layoutParams.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    windowManager.updateViewLayout(rootView, layoutParams)
                    true
                }
                else -> false
            }
        }

        engine.lifecycleChannel.appIsResumed()

        // 为悬浮窗加入布局
        rootView.findViewById<FrameLayout>(R.id.floating_window)
            .addView(
                flutterView,
                ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT
                )
            )
        windowManager.updateViewLayout(rootView, layoutParams)
    }

    private fun setPosition() {
        // 设置位置
        val screenWidth = metrics.widthPixels
        val screenHeight = metrics.heightPixels
        layoutParams.x = (screenWidth - layoutParams.width) / 2
        layoutParams.y = (screenHeight - layoutParams.height) / 2

        windowManager.addView(rootView, layoutParams)
        flutterView = FlutterView(inflater.context, FlutterSurfaceView(inflater.context, true))
        flutterView.attachToFlutterEngine(engine)
    }
}
  • 唤起悬浮窗组件
    直接通过adb指令唤起即可

adb shell am start-foreground-service -n com.karl.open.desktop_app/com.karl.open.desktop_app.WindowsService

  • 注意
  1. 通过服务唤起悬浮窗,Android要求必须是系统应用,因此大家在使用的时候还需要配置下系统签名
  2. Flutter engine必须使用FlutterEngineGroup进行托管,否则静置一段时间后,engine就会被系统回收!!!

关于灵动岛的一些思考

windows版本的灵动岛组件,实现起来其实是比较简单的。但是我在思考,假设我作为一个OS开发者,我该怎么看待iPhone的这个软件创新?

  1. iPhone这个灵动岛的问世,其实把用户对状态栏的认知颠覆了:原来平时用来看时间电量的地方还能这么玩;这个创新能否带动整个移动端、甚至桌面端状态栏等工具的改革
  2. 虽说创新,但目前从各种测评来看,这个工具很少有应用接入,连iOS自己的软件都很多没有接入。着实是有点鸡肋的,而且用户还要去学习如何使用这个灵动岛,当应用更多的接入进来,用户的教育成本会变得更高,降低使用体验。所以iPhone为啥敢开拓创新做这个至少目前很鸡肋的工具呢?
  3. iPhone官方如何去推广灵动岛,让更多用户接受

上面这几个问题,也是我一直在思考的。但其实是环环相扣的,首先能否引领新的交互改革,这个取决于市场的接受度。而市场的接受度,除了果粉引以为傲的“别人没有而我有”,还要做到真正的实用:iOS自身更多的软件接入,让灵动岛功能更完善。
用户习惯了用这个工具,大量软件就必须为了用户而作
同时按照iPhone的营销手段,会大量利用iPhone的用户心理,不断放大这个灵动岛的格调,很多软件为了俘获用户,甚至会专门为灵动岛做一些扩充的功能,从而吸引很多用户。【目前已有一些软件在做这个事情了】

而假设我是OS开发者,如果我要去做这个工具,首先我的用户基数要足够大,同时让工具提供简单且实用的功能,真正把投入产出比做好,而且真正得服务于用户。酷炫与否交给营销去推广,真正对用户有用的东西,才是底子所在!

写在最后

灵动岛组件的实现,分windows和android系统。
项目源码仓库:github.com/WxqKb/open_…

作者:Karl_wei
链接:https://juejin.cn/post/7154420798059446309

文末福利

为帮助大家学习 Flutter这个全新的跨平台技术、掌握其背后的框架原理和底层设计思想,建立起属于自己的知识体系,这里特意联合谷歌技术团队共同整理了一份Flutter全家桶学习资料。包含Flutter技术解析与实战、Flutter进阶学习笔记、Flutter入门与实战和Flutter完整开发实战详解(有完整文档的伙伴可点击文末卡片查看获取方式!)

《Flutter技术解析与实战》

目录

img

第一章 混合工程

  • Flutter工程体系
  • 混合工程改造实战
  • 混合工程与持续集成
  • 快速完成混合工程搭建
  • 使用混合栈框架开发

img

第二章 能力增强

  • 基于原生能力的插件扩展
  • 基于外接纹理的同层渲染
  • 多媒体能力扩展实践
  • 富文本能力应用实践

img

第三章 业务架构设计

  • 应用框架设计实践
  • 轻量级动态化渲染引擎的设计
  • 面向切面编程的设计实践
  • 高性能的动态模板渲染实践

img

第四章 数据统计与性能

  • 数据统计框架的设计
  • 性能稳定性监控方案的设计
  • 高可用框架的设计与实践
  • 跨端方案性能对比实践

img

第五章 企业级应用实战

  • 基于Flutter的端结构演进与创新
  • Flutter与FaaS云端一体化架构

img

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Flutter桌面小工具 -- 灵动岛【Windows+Android版本】 的相关文章

随机推荐

  • android 串口开发

    FT312D有参考
  • AD批量隐藏元件注释(Comment)

    步骤 1 右击元件注释在菜单中选中 Find Similar Objects 查找相似元件 2 在弹出来的菜单中 在Part comment 后面的下拉框中选择Same 即设定筛选条件为筛选所有相同comment的元件 然后点击OK 最后点
  • HttpComponents入门解析

    1 简介 超文本传输协议 http 是目前互联网上极其普遍的传输协议 它为构建功能丰富 绚丽多彩的网页提供了强大的支持 构建一个网站 通常无需直接操作http协议 目前流行的WEB框架已经透明的将这些底层功能封装的很好了 如常见的J2EE
  • java 关于锁常见面试题

    1 synchronized作用于静态方法和非静态方法的区别 非静态方法 给对象加锁 可以理解为给这个对象的内存上锁 注意 只是这块内存 其他同类对象都会有各自的内存锁 这时候在其他一个以上线程中执行该对象的这个同步方法 注意 是该对象 就
  • 「考生说」双非考生依旧可以成为“天选之子”

    随着2022年考研复试季的结束 微博热搜开始被 六战法考最终上岸的北大物业小哥刘政 中专生考进清华 等词条刷屏 大批网友也开启了他们 夸夸 能力 在这大量的评论中依旧有着众多让人鼓舞的金句产出 你不需要很厉害才能开始 但你需要开始才能很厉害
  • 【计算机图形学课程】二.MFC鼠标响应函数模拟画图软件

    上一篇文章我们讲述MFC绘制图形的基本函数 包括绘制直线 绘制矩形 绘制椭圆及绘制文字 同时通过绕圆旋转和矩形平移简单的理解了图形学知识 这篇文章我将介绍鼠标响应和键盘响应 通过这些事件让学生实现一个类似画图的简单软件 同时充分发挥学生想象
  • 【新手教程】Windows本地化安装、运行,部署Auto-GPT

    Windows安装 运行Auto GPT 第一 准备条件 OpenAI Key 请登录官网获取 sk RhLoBodCbL6AAlyuYeC8T3BlbkFJ5vJfX9P5Md504SmADtth 第二 环境搭建 2 1安装python
  • 创建Vue项目(demo)教程

    如何创建个人Demo 1 环境准备 安装node js node官网 https nodejs org zh cn 安装完成后查看版本 打开 cmd 命窗口 使用 node v 查看版本号 出现版本号即安装成功 node v npm v 安
  • 怎么用C#获取指定窗口的句柄(笔记)

    获取鼠标位置处窗口句柄 需要使用到Win32Api函数WindowFromPoint 用来根据坐标获取窗口句柄 C 引用如下 DllImport user32 dll EntryPoint WindowFromPoint 指定坐标处窗体句柄
  • 不知道为什么的为什么

    我最想的是什么仔细想了想 静悄悄的从这个世界无声无息的消失 不想有任何牵连 任何羁绊
  • 随机生成Long值

    随机生成Long类型的数值 public class RandomLongUtil 随机生成Long值 param bit 位数 return 返回Long值 throws Exception 异常 public static Long r
  • STM32PWM控制智能风扇

    设计思路 这个是一个STM32通过定时器产生PWM波控制小风扇的设计 首先STM32驱动ds18b20温度传感器采集环境温度 然后通过按键设置温度的阈值 不同的温度范围定时器产生的PWM波不同 相应的小风扇的转速也会不一样 温度越高 小风扇
  • Spring属性注入方式

    1 Spring也表示一个开源框架 是为了解决企业程序应用开发的复杂性 框架的主要优势之一就是其分层架构 分层架构允许使用者选择使用哪一个组件 同时为J2EE应用程序开发提供集成的框架 Spring使用基本的bean来完成以前只能由EJB完
  • idea乱码解决方式大汇总

    目录 idea版本 解决方法 一 基本方法 1 File gt Settings gt Editor 2 二 Maven乱码解决方法 三 运行时乱码解决方法 四 因为以前乱设置导致的乱码 idea版本 解决方法 一 基本方法 1 File
  • 华为telnet学习笔记

    华为telnet用户密码aaa模式 配置完接口后 aaa local user admin password cipher cisco 创建用户设置账号密码 local user admin service type telnet 为这个用
  • QLUACM暑假训练5 C题题解

    C题题目大意 给一个n行m列的矩阵 矩阵的每个元素由 或者 填充 如果一行或者一列都由 构成 则删除这一行或者这一列 最后按照相对位置输出剩余的元素 题解 题目思路 1 我们需要找出一行或者一列都由 构成的行和列的位置 也就是我们需要找到没
  • 图像分割套件PaddleSeg全面解析(五)模型与Backbone代码解读

    本章节将介绍PaddleSeg的核心部分 分割模型和主干网络部分 在yaml配置文件中有以下定义 模型信息 model 模型的类型FCN type FCN 使用的主干网络为HRNet backbone type HRNet W18 主干网络
  • 宋浩高等数学笔记(六)定积分的应用

    本章继续更新高数笔记 6 5节的物理题暂不更新 有需求的同学自行看课
  • R语言:创建数据集

    文章目录 1 创建数据集 1 1 数据集的概念 1 2 数据结构 1 2 1 向量 1 2 2 矩阵 1 2 3 数组 1 2 4 数据框 data frame 的切片 attach detach 和with 实例标识符 1 2 5 因子
  • Flutter桌面小工具 -- 灵动岛【Windows+Android版本】

    通过此篇文章 你将了解到 Flutter动画实现灵动岛 Flutter如何开发一个置顶可自由拖拽的小工具 分享一些关于灵动岛的想法 本文为稀土掘金技术社区首发签约文章 14天内禁止转载 14天后未获授权禁止转载 侵权必究 前言 Flutte