Android开发Flutter 桌面应用窗口化实战示例

目录
  • 前言
  • 一、应用窗口的常规配置
    • 应用窗口化
    • 自定义窗口导航栏
    • 美化应用窗口
  • 二、windows平台特定交互
    • 注册表操作
    • 执行控制台指令
    • 实现应用单例
  • 三、桌面应用的交互习惯
    • 按钮点击态
    • 获取应用启动参数
  • 四、写在最后

前言

通过此篇文章,你可以编写出一个完整桌面应用的窗口框架。

你将了解到:

  • Flutter在开发windows和Android桌面应用初始阶段,应用窗口的常规配置;
  • windows平台特定交互的实现,如:执行控制台指令,windows注册表,应用单例等;
  • 桌面应用的交互习惯,如:交互点击态,不同大小的页面切换,获取系统唤起应用的参数等。

在使用Flutter开发桌面应用之前,笔者之前都是开发移动App的,对于移动应用的交互比较熟悉。开始桌面应用开发后,我发现除了技术栈一样之外,其他交互细节、用户行为习惯以及操作系统特性等都有很大的不同。

我将在windows和android桌面设备上,从0到1亲自搭建一个开源项目,并且记录实现细节和技术难点。

一、应用窗口的常规配置

众所周知,Flutter目前最大的应用是在移动app上,在移动设备上都是以全屏方式展示,因此没有应用窗口这个概念。而桌面应用是窗口化的,需求方一般都会对窗口外观有很高的要求,比如:自定义窗口导航栏、设置圆角、阴影;同时还有可能要禁止系统自动放大的行为。

应用窗口化

Flutter在windows桌面平台,是依托于Win32Window承载engine的,而Win32Windows本身就是窗口化的,无需再做过多的配置。(不过也正因为依托原生窗口,作为UI框架的flutter完全没办法对Win32Window的外观做任何配置)

// win32_window.cpp
bool Win32Window::CreateAndShow(const std::wstring& title,
                                const Point& origin,
                                const Size& size) {
 // ...此处省略代码...
 // 这里创建了win32接口的句柄
  HWND window = CreateWindow(
      window_class, title.c_str(), WS_POPUP | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX,
      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
      Scale(size.width, scale_factor), Scale(size.height, scale_factor),
      nullptr, nullptr, GetModuleHandle(nullptr), this);
  UpdateWindow(window);
  if (!window) {
    return false;
  }
  return OnCreate();
}
bool FlutterWindow::OnCreate() {
  if (!Win32Window::OnCreate()) {
    return false;
  }
  // GetClientArea获取创建的win32Window区域
  RECT frame = GetClientArea();
  // 绑定窗口和flutter engine
  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
      frame.right - frame.left, frame.bottom - frame.top, project_);
  if (!flutter_controller_->engine() || !flutter_controller_->view()) {
    return false;
  }
  RegisterPlugins(flutter_controller_->engine());
  SetChildContent(flutter_controller_->view()->GetNativeWindow());
  return true;
}

应用窗口化主要是针对Android平台,Flutter应用是依托于Activity的,Android平台上Activity默认是全屏,且出于安全考虑,当一个Activity展示的时候,是不允许用户穿透点击的。所以想要让Flutter应用在Android大屏桌面设备上展示出windows上的效果,需要以下步骤:

  • 将底层承载的FlutterActivity的主题样式设置为Dialog,同时全屏窗口的背景色设置为透明,点击时Dialog不消失
<!-- android/app/src/main/res/values/styles.xml -->
<style name="Theme.DialogApp" parent="Theme.AppCompat.Light.Dialog">
    <item name="android:windowBackground">@drawable/launch_application</item>
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:backgroundDimEnabled">false</item>
    <item name="windowActionBar">false</item>
    <item name="windowNoTitle">true</item>
</style>
<!-- android/app/src/main/AndroidManifest.xml -->
<activity
android:name=".MainActivity"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/Theme.DialogApp"
android:windowSoftInputMode="adjustResize">
    <meta-data
        android:name="io.flutter.embedding.android.NormalTheme"
        android:resource="@style/Theme.DialogApp" />
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
// android/app/src/main/kotlin/com/maxhub/upgrade_assistant/MainActivity.kt
class MainActivity : FlutterActivity() {
    override fun getTransparencyMode(): TransparencyMode {
        // 设置窗口背景透明
        return TransparencyMode.transparent
    }
    override fun onResume() {
        super.onResume()
        setFinishOnTouchOutside(false) // 点击外部,dialog不消失
        // 设置窗口全屏
        var lp = window.attributes
        lp.width = -1
        lp.height = -1
        window.attributes = lp
    }
}
  • 至此Android提供了一个全屏的透明窗口,Flutter runApp的时候,我在MaterialApp外层套了一个盒子控件,这个控件内部主要做边距、阴影等一系列窗口化行为。
class GlobalBoxManager extends StatelessWidget {
  GlobalBoxManager({Key? key, required this.child}) : super(key: key);
  final Widget child;
  @override
  Widget build(BuildContext context) {
    return Container(
        width: ScreenUtil().screenWidth,
        height: ScreenUtil().screenHeight,
        // android伪全屏,加入边距
        padding: EdgeInsets.symmetric(horizontal: 374.w, vertical: 173.h),
        child: child,
    );
  }
}
// MyApp下的build构造方法
GlobalBoxManager(
  child: GetMaterialApp(
    locale: Get.deviceLocale,
    translations: Internationalization(),
    // 桌面应用的页面跳转习惯是无动画的,符合用户习惯
    defaultTransition: Transition.noTransition,
    transitionDuration: Duration.zero,
    theme: lightTheme,
    darkTheme: darkTheme,
    initialRoute: initialRoute,
    getPages: RouteConfig.getPages,
    title: 'appName'.tr,
  ),
),
  • 效果图

自定义窗口导航栏

主要针对Windows平台,原因上面我们解析过:win32Window是在windows目录下的模板代码创建的默认是带系统导航栏的(如下图)。

很遗憾Flutter官方也没有提供方法,pub库上对窗口操作支持的最好的是window_manager,由国内Flutter桌面开源社区leanFlutter所提供。

  • yaml导入window_manager,在runApp之前执行以下代码,把win32窗口的导航栏去掉,同时配置背景色为透明、居中显示;
dependencies:
  flutter:
    sdk: flutter
  window_manager: ^0.2.6
// runApp之前运行
WindowManager w = WindowManager.instance;
await w.ensureInitialized();
WindowOptions windowOptions = WindowOptions(
  size: normalWindowSize,
  center: true,
  titleBarStyle: TitleBarStyle.hidden // 该属性隐藏导航栏
);
w.waitUntilReadyToShow(windowOptions, () async {
  await w.setBackgroundColor(Colors.transparent);
  await w.show();
  await w.focus();
  await w.setAsFrameless();
});
  • 此时会发现应用打开时在左下角闪一下再居中。这是由于原生win32窗口默认是左上角显示,而后在flutter通过插件才居中;
  • 处理方式建议在原生代码中先把窗口设为默认不显示,通过上面的window_manager.show()展示出来;
// windows/runner/win32_window.cpp
HWND window = CreateWindow(
    // 去除WS_VISIBLE属性
    window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
    Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
    Scale(size.width, scale_factor), Scale(size.height, scale_factor),
    nullptr, nullptr, GetModuleHandle(nullptr), this);

美化应用窗口

通过前面的步骤,我们在android和windows平台上都得到了一个安全透明的窗口,接下来的修饰Flutter就可以为所欲为了。

  • 窗口阴影、圆角

上面介绍过在MaterialApp外套有盒子控件,直接在Container内加入阴影和圆角即可,不过Android和桌面平台还是需要区分下的;

import 'dart:io';
import 'package:flutter/material.dart';
class GlobalBoxManager extends StatelessWidget {
  const GlobalBoxManager({Key? key, required this.child}) : super(key: key);
  final Widget child;
  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: double.infinity,
      // android伪全屏,加入边距
      padding: Platform.isAndroid
          ? const EdgeInsets.symmetric(horizontal: 374, vertical: 173)
          : EdgeInsets.zero,
      child: Container(
        clipBehavior: Clip.antiAliasWithSaveLayer,
        margin: const EdgeInsets.all(10),
        decoration: const BoxDecoration(
            borderRadius: BorderRadius.all(Radius.circular(8)),
            boxShadow: [
              BoxShadow(color: Color(0x33000000), blurRadius: 8),
            ]),
        child: child,
      ),
    );
  }
}

  • 自定义导航栏

回归Scaffold的AppBar配置,再加上导航拖拽窗口事件(仅windows可拖拽)

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: PreferredSize(
      preferredSize: const Size.fromHeight(64),
      child: GestureDetector(
        behavior: HitTestBehavior.translucent,
        onPanStart: (details) {
          if (Platform.isWindows) windowManager.startDragging();
        },
        onDoubleTap: () {},
        child: AppBar(
          title: Text(widget.title),
          centerTitle: true,
          actions: [
            GestureDetector(
              behavior: HitTestBehavior.opaque,
              child: const Padding(
                padding: EdgeInsets.symmetric(horizontal: 16),
                child: Icon(
                  Icons.close,
                  size: 24,
                ),
              ),
            ),
          ],
        ),
      ),
    ),
    body: Center(),
  );
}

到这里多平台的窗口就配置好了,接下来可以愉快的编写页面啦。

可能有些小伙伴会说:窗口的效果本就应该由原生去写,为啥要让Flutter去做这么多的事情?

答案很简单:

跨平台! 要跨平台就势必需要绕一些,通过这种方式你会发现任何平台的应用,都可以得到相同效果的窗口,而代码只需要Flutter写一次,这才是Flutter存在的真正意义。

二、windows平台特定交互

在开发windows的过程中,我发现跟移动app最大的不同在于:桌面应用需要频繁的去与系统做一些交互。

注册表操作

应用开发过程中,经常需要通过注册表来做数据存储;在pub上也有一个库提供这个能力,但是我没有使用,因为dart已经提供了win32相关的接口,我认为这个基础的能力没必要引用多一个库,所以手撸了一个工具类来操作注册表。(值得注意的是部分注册表的操作是需要管理员权限的,所以应用提权要做好)

import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
const maxItemLength= 2048;
class RegistryKeyValuePair {
  final String key;
  final String value;
  const RegistryKeyValuePair(this.key, this.value);
}
class RegistryUtil {
  /// 根据键名获取注册表的值
  static String? getRegeditForKey(String regPath, String key,
      {int hKeyValue = HKEY_LOCAL_MACHINE}) {
    var res = getRegedit(regPath, hKeyValue: hKeyValue);
    return res[key];
  }
  /// 设置注册表值
  static setRegeditValue(String regPath, String key, String value,
      {int hKeyValue = HKEY_CURRENT_USER}) {
    final phKey = calloc<HANDLE>();
    final lpKeyPath = regPath.toNativeUtf16();
    final lpKey = key.toNativeUtf16();
    final lpValue = value.toNativeUtf16();
    try {
      if (RegSetKeyValue(hKeyValue, lpKeyPath, lpKey, REG_SZ, lpValue,
              lpValue.length * 2) !=
          ERROR_SUCCESS) {
        throw Exception("Can't set registry key");
      }
      return phKey.value;
    } finally {
      free(phKey);
      free(lpKeyPath);
      free(lpKey);
      free(lpValue);
      RegCloseKey(HKEY_CURRENT_USER);
    }
  }
  /// 获取注册表所有子项
  static List<String>? getRegeditKeys(String regPath,
      {int hKeyValue = HKEY_LOCAL_MACHINE}) {
    final hKey = _getRegistryKeyHandle(hKeyValue, regPath);
    var dwIndex = 0;
    String? key;
    List<String>? keysList;
    key = _enumerateKeyList(hKey, dwIndex);
    while (key != null) {
      keysList ??= [];
      keysList.add(key);
      dwIndex++;
      key = _enumerateKeyList(hKey, dwIndex);
    }
    RegCloseKey(hKey);
    return keysList;
  }
  /// 删除注册表的子项
  static bool deleteRegistryKey(String regPath, String subPath,
      {int hKeyValue = HKEY_LOCAL_MACHINE}) {
    final subKeyForPath = subPath.toNativeUtf16();
    final hKey = _getRegistryKeyHandle(hKeyValue, regPath);
    try {
      final status = RegDeleteKey(hKey, subKeyForPath);
      switch (status) {
        case ERROR_SUCCESS:
          return true;
        case ERROR_MORE_DATA:
          throw Exception('An item required more than $maxItemLength bytes.');
        case ERROR_NO_MORE_ITEMS:
          return false;
        default:
          throw Exception('unknown error');
      }
    } finally {
      RegCloseKey(hKey);
      free(subKeyForPath);
    }
  }
  /// 根据项的路径获取所有值
  static Map<String, String> getRegedit(String regPath,
      {int hKeyValue = HKEY_CURRENT_USER}) {
    final hKey = _getRegistryKeyHandle(hKeyValue, regPath);
    final Map<String, String> portsList = <String, String>{};
    /// The index of the value to be retrieved.
    var dwIndex = 0;
    RegistryKeyValuePair? item;
    item = _enumerateKey(hKey, dwIndex);
    while (item != null) {
      portsList[item.key] = item.value;
      dwIndex++;
      item = _enumerateKey(hKey, dwIndex);
    }
    RegCloseKey(hKey);
    return portsList;
  }
  static int _getRegistryKeyHandle(int hive, String key) {
    final phKey = calloc<HANDLE>();
    final lpKeyPath = key.toNativeUtf16();
    try {
      final res = RegOpenKeyEx(hive, lpKeyPath, 0, KEY_READ, phKey);
      if (res != ERROR_SUCCESS) {
        throw Exception("Can't open registry key");
      }
      return phKey.value;
    } finally {
      free(phKey);
      free(lpKeyPath);
    }
  }
  static RegistryKeyValuePair? _enumerateKey(int hKey, int index) {
    final lpValueName = wsalloc(MAX_PATH);
    final lpcchValueName = calloc<DWORD>()..value = MAX_PATH;
    final lpType = calloc<DWORD>();
    final lpData = calloc<BYTE>(maxItemLength);
    final lpcbData = calloc<DWORD>()..value = maxItemLength;
    try {
      final status = RegEnumValue(hKey, index, lpValueName, lpcchValueName,
          nullptr, lpType, lpData, lpcbData);
      switch (status) {
        case ERROR_SUCCESS:
          {
            // if (lpType.value != REG_SZ) throw Exception('Non-string content.');
            if (lpType.value == REG_DWORD) {
              return RegistryKeyValuePair(lpValueName.toDartString(),
                  lpData.cast<Uint32>().value.toString());
            }
            if (lpType.value == REG_SZ) {
              return RegistryKeyValuePair(lpValueName.toDartString(),
                  lpData.cast<Utf16>().toDartString());
            }
            break;
          }
        case ERROR_MORE_DATA:
          throw Exception('An item required more than $maxItemLength bytes.');
        case ERROR_NO_MORE_ITEMS:
          return null;
        default:
          throw Exception('unknown error');
      }
    } finally {
      free(lpValueName);
      free(lpcchValueName);
      free(lpType);
      free(lpData);
      free(lpcbData);
    }
    return null;
  }
  static String? _enumerateKeyList(int hKey, int index) {
    final lpValueName = wsalloc(MAX_PATH);
    final lpcchValueName = calloc<DWORD>()..value = MAX_PATH;
    try {
      final status = RegEnumKeyEx(hKey, index, lpValueName, lpcchValueName,
          nullptr, nullptr, nullptr, nullptr);
      switch (status) {
        case ERROR_SUCCESS:
          return lpValueName.toDartString();
        case ERROR_MORE_DATA:
          throw Exception('An item required more than $maxItemLength bytes.');
        case ERROR_NO_MORE_ITEMS:
          return null;
        default:
          throw Exception('unknown error');
      }
    } finally {
      free(lpValueName);
      free(lpcchValueName);
    }
  }
}

执行控制台指令

windows上,我们可以通过cmd指令做所有事情,dart也提供了这种能力。我们可以通过io库中的Progress类来运行指令。如:帮助用户打开网络连接。

Process.start('ncpa.cpl', [],runInShell: true);

刚接触桌面开发的小伙伴,真的很需要这个知识点。

实现应用单例

应用单例是windows需要特殊处理,android默认是单例的。而windows如果不作处理,每次点击都会重新运行一个应用进程,这显然不合理。Flutter可以通过windows_single_instance插件来实现单例。在runApp之前执行下这个方法,重复点击时会让用户获得焦点置顶,而不是多开一个应用。

/// windows设置单实例启动
static setSingleInstance(List<String> args) async {
  await WindowsSingleInstance.ensureSingleInstance(args, "desktop_open",
      onSecondWindow: (args) async {
    // 唤起并聚焦
    if (await windowManager.isMinimized()) await windowManager.restore();
    windowManager.focus();
  });
}

三、桌面应用的交互习惯

按钮点击态

按钮点击交互的状态,其实在移动端也存在。但不同的是移动端的按钮基本上水波纹的效果就能满足用户使用,但是桌面应用显示区域大,而点击的鼠标却很小,很多时候点击已经过去但水波纹根本就没显示出来。

正常交互是:点击按钮马上响应点击态的颜色(文本和背景都能编),松开恢复。

TextButton(
  clipBehavior: Clip.antiAliasWithSaveLayer,
  style: ButtonStyle(
    animationDuration: Duration.zero, // 动画延时设置为0
    visualDensity: VisualDensity.compact,
    overlayColor: MaterialStateProperty.all(Colors.transparent),
    padding: MaterialStateProperty.all(EdgeInsets.zero),
    textStyle:
        MaterialStateProperty.all(Theme.of(context).textTheme.subtitle1),
    // 按钮按下的时候的前景色,会让文本的颜色按下时变为白色
    foregroundColor: MaterialStateProperty.resolveWith((states) {
      return states.contains(MaterialState.pressed)
          ? Colors.white
          : Theme.of(context).toggleableActiveColor;
    }),
    // 按钮按下的时候的背景色,会让背景按下时变为蓝色
    backgroundColor: MaterialStateProperty.resolveWith((states) {
      return states.contains(MaterialState.pressed)
          ? Theme.of(context).toggleableActiveColor
          : null;
    }),
  ),
  onPressed: null,
  child: XXX),
)

获取应用启动参数

由于我们的桌面设备升级自研的整机,因此在开发过程经常遇到其他软件要唤起Flutter应用的需求。那么如何唤起,又如何拿到唤起参数呢?

1. windows:其他应用通过Procress.start启动.exe即可运行Flutter的软件;传参也非常简单,直接.exe后面带参数,多个参数使用空格隔开,然后再Flutter main函数中的args就能拿到参数的列表,非常方便。

其实cmd执行的参数,是被win32Window接收了,只是Flutter帮我们做了这层转换,通过engine传递给main函数,而Android就没那么方便了。

2. Android:Android原生启动应用是通过Intent对应包名下的Activity,然后再Activity中通过Intent.getExtra可以拿到参数。我们都知道Android平台下Flutter只有一个Activity,因此做法是先在MainActivity中拿到Intent的参数,然后建立Method Channel通道;

``` kotlin class MainActivity : FlutterActivity() { private var sharedText: String? = null private val channel = "app.open.shared.data"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val intent = intent
        handleSendText(intent) // Handle text being sent
    }
    override fun onRestart() {
        super.onRestart()
        flutterEngine!!.lifecycleChannel.appIsResumed()
    }
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel)
            .setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
                when (call.method) {
                    "getSharedText" -> {
                        result.success(sharedText)
                    }
                }
            }
    }
    private fun handleSendText(intent: Intent) {
        sharedText = intent.getStringExtra("params")
    }
}
```
Flutter层在main函数中通过Method Channel的方式取到MainActivity中存储的参数,绕多了一层链路。
```dart
const platform = MethodChannel('app.open.shared.data');
String? sharedData = await platform.invokeMethod('getSharedText');
if (sharedData == null) return null;
return jsonDecode(sharedData);
```

四、写在最后

通过上面这么多的实现,我们已经完全把一个应用窗体结构搭建起来了。长篇幅的实战记录,希望可以切实的帮助到大家。总体来说,桌面开发虽然还有很多缺陷,但是能用,性能尚佳,跨平台降低成本。

以上就是Android开发Flutter 桌面应用窗口化实战示例的详细内容,更多关于Android Flutter 桌面应用窗口化的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android开发中Flutter组件实用技巧

    目录 正文 简化 Assert 管理 更容易 imports 从按钮上移除飞溅效果 更简单的平台小工具 可见性小工具 正文 今天我将向您展示 4 个非常有用的 Flutter 技巧,您可以立即应用到您的项目.我不会向您展示任何包或扩展,就像我通常做的那样,但是非常简单,但是非常有用的提示! 简化 Assert 管理 管理 Assert 可能非常困难.如果你想在你的应用程序中多次使用一个图像,你必须一次又一次地指定路径.但是有一个简单得多的解决方案.创建一个 App Assets 类,用于存储所有

  • Android Flutter表格组件Table的使用详解

    目录 Table.TabRow.TabCell 小结 之前开发中用到的表格,本篇文章主要介绍如何在页面中使用表格做一个记录. Table组件不同于其它Flex布局,它是直接继承的RenderObjectWidget的.相当于是一个独立的组件,区别与其他系列组件. Table.TabRow.TabCell 惯例,先看下Table相关的构造方法: Table({ Key? key, this.children = const <TableRow>[],//行列表 表示多少行 this.column

  • Android Flutter实现GIF动画效果的方法详解

    目录 前言 交错动画机制 代码实现 Interval 介绍 总结 前言 我们之前介绍了不少有关动画的篇章.前面介绍的动画都是只有一个动画效果,那如果我们想对某个组件实现一组动效,比如下面的效果,该怎么办? staggered animation 这个时候我们需要用到组合动效, Flutter 提供了交错动画(Staggered Animation)的方式实现.对于多个 Anmation 对象,可以共用一个 AnimationController,然后在不同的时间段执行动画效果.这就有点像 GIF

  • Android Flutter实现页面切换转场动画效果

    目录 前言 Hero 动画过程 Hero 基础示例 总结 前言 写了一篇基础的性能优化的内容,继续我们的动画相关的介绍.今天的主角是英雄 —— Hero 组件.Hero 组件非常适合从列表.概览页切换到详情页转场动画场合.因为可以将两个页面的组件串起来动画,体验上会觉得整个操作的连贯性非常好.下面是我们这篇要做的一个效果. 屏幕录制2021-11-09 下午9.39.49.gif Hero 动画过程 Hero 本质是是在不同的路由页面做了一个中转层,然后通过动画完成过渡,下面用4张图是官方演示的

  • Android开发组件flutter的20个常用技巧示例总结

    目录 1.map遍历快速实现边距,文字自适应改变大小 2.使用SafeArea 添加边距 3.布局思路 4.获取当前屏幕的大小 5.文本溢出显示省略号 6.一个圆角带搜索icon的搜索框案例 7.修改按钮的背景色 8.tab切换实例 9.点击事件组件点击空白区域不触发点击 10.使用主题色 11.往安卓模拟器中传图片 12.控制text的最大行数显示影藏文字 13.去掉默认的抽屉图标 14.图片占满屏 15.倒计时 16.固定底部 17.添加阴影 18.隐藏键盘 19.获取父级组件大小 20.点

  • Android Flutter实现有趣的页面滚动效果

    目录 CustomScrollView 简介 改造原代码 让导航栏更有趣 改造后的代码 其他效果 总结 在Flutter 高仿一个某支付价值几个亿的页面这一篇中,我们使用了 ListView 将几个 GridView 组合在一起实现了不同可滑动组件的粘合,但是这里必须要设置禁止 GridView 的滑动,防止多个滑动组件的冲突.这种方式写起来不太方便,事实上 Flutter 提供了 CustomScrollView 来粘合多个滑动组件,并且可以实现更有趣的滑动效果. CustomScrollVi

  • Android开发Flutter 桌面应用窗口化实战示例

    目录 前言 一.应用窗口的常规配置 应用窗口化 自定义窗口导航栏 美化应用窗口 二.windows平台特定交互 注册表操作 执行控制台指令 实现应用单例 三.桌面应用的交互习惯 按钮点击态 获取应用启动参数 四.写在最后 前言 通过此篇文章,你可以编写出一个完整桌面应用的窗口框架. 你将了解到: Flutter在开发windows和Android桌面应用初始阶段,应用窗口的常规配置: windows平台特定交互的实现,如:执行控制台指令,windows注册表,应用单例等: 桌面应用的交互习惯,如

  • Android开发TextvView实现镂空字体效果示例代码

    记录一下... 自定义TextView public class HollowTextView extends AppCompatTextView { private Paint mTextPaint, mBackgroundPaint; private Bitmap mBackgroundBitmap,mTextBitmap; private Canvas mBackgroundCanvas,mTextCanvas; private RectF mBackgroundRect; private

  • Android开发快速实现底部导航栏示例

    目录 Tint 着色器 依赖(AndroidX) 布局 编写渲染颜色选择器-tint_selector_menu_color menu 文件中 icon-nav_bottom_menu BottomNavigationView的点击事件 配合ViewPager实现Tab栏 对应的适配器 Tint 着色器 优点:去除“无用”图片,节省空间 配合BottomNavigationView,实现一个快速,简洁的Tab栏 传统做法:Tab 切换,字体变色.图片变色.至少给我提供八张图,四张默认,四张选中,

  • Android利用Flutter path绘制粽子的示例代码

    目录 前言 绘制 基本轮廓 粽叶 嘴巴 眼睛 腮红 手&脚 头巾 咸甜是一家 发声 动画控制嘴巴开合 用到的技术点 总结 前言 大家好,端午将至,首先提前祝小伙伴端午安康,端午作为中华民族的非常重要的传统节日,粽子那是必不可少的,但是你真的知道粽子的历史吗? 今天跟随本篇文章用Flutter path画一个会科普节日的的粽子吧- 绘制 基本轮廓 首先我们需要将粽子的基本轮廓绘制出来,通过图片可以看到粽子的轮廓是一个圆圆的三角形状, 本篇文章所有的图形都是用纯Path路径制作,这里我们可以将粽子的

  • Android开发两个activity之间传值示例详解

    目录 使用Inten的putExtra传递 使用Intention的Bundle传递 使用Activity销毁时传递数据 SharedPreferences传递数据 使用序列化对象Seriazable 使用静态变量传递数据 handler 使用Inten的putExtra传递 第一个Activity中 //创建意图对象 Intent intent = new Intent(this,MainActivity2.class); //设置传递键值对 intent.putExtra("name&quo

  • Android开发使用WebView打造web app示例代码

    目录 前言 代码如下 前言 博主最近想做一款app,因为内容已经有了,故想到了使用WebView来做 ,现将代码贴出如下,供有同样需求的人参考,少走弯路 代码如下 public class MainActivity extends Activity{ private WebView webview; private Handler handler; private ProgressDialog pd; @Override public void onCreate(Bundle savedInst

  • Android开发实现各种图形绘制功能示例

    本文实例讲述了Android开发实现各种图形绘制功能.分享给大家供大家参考,具体如下: 这里结合本人的开发事例,简单介绍一下如何在Android平台下实现各种图形的绘制. 首先自定义一个View类,这个view类里面需要一个Paint对象来控制图形的属性,需要一个Path对象来记录图形绘制的路径,需要一个Canvas类来执行绘图操作,还需要一个Bitmap类来盛放绘画的结果. Paint mPaint = new Paint(); mPaint.setAntiAlias(true); mPain

  • Android开发中CheckBox的简单用法示例

    本文实例讲述了Android开发中CheckBox的简单用法.分享给大家供大家参考,具体如下: CheckBox是一种在界面开发中比较常见的控件,Android中UI开发也有CheckBox,简单的说下它的使用,每个CheckBox都要设置监听,设置的监听为CompouButton.OnCheckedChangedListener(). package com.zhuguangwei; import android.app.Activity; import android.os.Bundle;

  • Android开发入门环境快速搭建实战教程

    前言 很多朋友都想开始自己的Android开发之旅,但是遇到困难重重.从最开始接触Android开发,从搭建开发环境就花了我大部分时间.所以,作为Android开发第一步,开发环境的搭建,显得基础而重要,下面介绍一种快速搭建Android开发环境的方法,以帮助更多朋友快速上手.话不多说了,来一起看看详细的介绍吧. 方法如下: 在开始之前,我们首先需要了解,当前开发android使用的主流开发平台为eclipse,因此本文讨论的是基于eclipse来做的. 具体需要的各个文件(软件)如下: Ecl

  • Android开发实现广告无限循环功能示例

    本文实例讲述了Android开发实现广告无限循环功能.分享给大家供大家参考,具体如下: 一.效果图: 二.代码实现: /** * 新闻首页 * * @Project App_Card * @Package com.android.koomama.fragment.home * @author chenlin * @version 1.0 * @Date 2014年6月22日 * @Note TODO */ public class NewsHomeFragment extends BaseFra

随机推荐