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

  1. Flutter动画完成灵动岛;
  2. Flutter怎么开发一个置顶可自在拖拽的小东西;
  3. 共享一些关于灵动岛的想法。

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

前言

Flutter开发Windows运用现已见怪不怪了,我觉得能够尝试做一些小东西。恰逢近期最近苹果iphone 14系列推出“灵动岛”,这个酷炫的组件瞬间引起许多重视;而且看到一些前端博客用css完成灵动岛的作用;作为Flutter的忠实拥护者,前端能写的Flutter有必要能写!

灵动岛作用完成

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

  • 小药丸扩大
    小药丸扩大的作用能够拆分为两步:横向扩大+惯性缩放回弹。需要两个动画和控制器,当扩大动画履行结束的时分,履行缩放动画,从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的小组件啦!

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

将运用装备为小东西【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_…