Android開發(fā)Flutter?桌面應(yīng)用窗口化實(shí)戰(zhàn)示例
前言
通過(guò)此篇文章,你可以編寫出一個(gè)完整桌面應(yīng)用的窗口框架。
你將了解到:
- Flutter在開發(fā)windows和Android桌面應(yīng)用初始階段,應(yīng)用窗口的常規(guī)配置;
- windows平臺(tái)特定交互的實(shí)現(xiàn),如:執(zhí)行控制臺(tái)指令,windows注冊(cè)表,應(yīng)用單例等;
- 桌面應(yīng)用的交互習(xí)慣,如:交互點(diǎn)擊態(tài),不同大小的頁(yè)面切換,獲取系統(tǒng)喚起應(yīng)用的參數(shù)等。
在使用Flutter開發(fā)桌面應(yīng)用之前,筆者之前都是開發(fā)移動(dòng)App的,對(duì)于移動(dòng)應(yīng)用的交互比較熟悉。開始桌面應(yīng)用開發(fā)后,我發(fā)現(xiàn)除了技術(shù)棧一樣之外,其他交互細(xì)節(jié)、用戶行為習(xí)慣以及操作系統(tǒng)特性等都有很大的不同。
我將在windows和android桌面設(shè)備上,從0到1親自搭建一個(gè)開源項(xiàng)目,并且記錄實(shí)現(xiàn)細(xì)節(jié)和技術(shù)難點(diǎn)。
一、應(yīng)用窗口的常規(guī)配置
眾所周知,F(xiàn)lutter目前最大的應(yīng)用是在移動(dòng)app上,在移動(dòng)設(shè)備上都是以全屏方式展示,因此沒(méi)有應(yīng)用窗口這個(gè)概念。而桌面應(yīng)用是窗口化的,需求方一般都會(huì)對(duì)窗口外觀有很高的要求,比如:自定義窗口導(dǎo)航欄、設(shè)置圓角、陰影;同時(shí)還有可能要禁止系統(tǒng)自動(dòng)放大的行為。
應(yīng)用窗口化
Flutter在windows桌面平臺(tái),是依托于Win32Window承載engine的,而Win32Windows本身就是窗口化的,無(wú)需再做過(guò)多的配置。(不過(guò)也正因?yàn)橐劳性翱?,作為UI框架的flutter完全沒(méi)辦法對(duì)Win32Window的外觀做任何配置)
// win32_window.cpp bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, const Size& size) { // ...此處省略代碼... // 這里創(chuàng)建了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獲取創(chuàng)建的win32Window區(qū)域 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; }
應(yīng)用窗口化主要是針對(duì)Android平臺(tái),F(xiàn)lutter應(yīng)用是依托于Activity的,Android平臺(tái)上Activity默認(rèn)是全屏,且出于安全考慮,當(dāng)一個(gè)Activity展示的時(shí)候,是不允許用戶穿透點(diǎn)擊的。所以想要讓Flutter應(yīng)用在Android大屏桌面設(shè)備上展示出windows上的效果,需要以下步驟:
- 將底層承載的FlutterActivity的主題樣式設(shè)置為Dialog,同時(shí)全屏窗口的背景色設(shè)置為透明,點(diǎn)擊時(shí)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 { // 設(shè)置窗口背景透明 return TransparencyMode.transparent } override fun onResume() { super.onResume() setFinishOnTouchOutside(false) // 點(diǎn)擊外部,dialog不消失 // 設(shè)置窗口全屏 var lp = window.attributes lp.width = -1 lp.height = -1 window.attributes = lp } }
- 至此Android提供了一個(gè)全屏的透明窗口,F(xiàn)lutter runApp的時(shí)候,我在MaterialApp外層套了一個(gè)盒子控件,這個(gè)控件內(nèi)部主要做邊距、陰影等一系列窗口化行為。
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構(gòu)造方法 GlobalBoxManager( child: GetMaterialApp( locale: Get.deviceLocale, translations: Internationalization(), // 桌面應(yīng)用的頁(yè)面跳轉(zhuǎn)習(xí)慣是無(wú)動(dòng)畫的,符合用戶習(xí)慣 defaultTransition: Transition.noTransition, transitionDuration: Duration.zero, theme: lightTheme, darkTheme: darkTheme, initialRoute: initialRoute, getPages: RouteConfig.getPages, title: 'appName'.tr, ), ),
- 效果圖
自定義窗口導(dǎo)航欄
主要針對(duì)Windows平臺(tái),原因上面我們解析過(guò):win32Window是在windows目錄下的模板代碼創(chuàng)建的默認(rèn)是帶系統(tǒng)導(dǎo)航欄的(如下圖)。
很遺憾Flutter官方也沒(méi)有提供方法,pub庫(kù)上對(duì)窗口操作支持的最好的是window_manager,由國(guó)內(nèi)Flutter桌面開源社區(qū)leanFlutter所提供。
- yaml導(dǎo)入window_manager,在runApp之前執(zhí)行以下代碼,把win32窗口的導(dǎo)航欄去掉,同時(shí)配置背景色為透明、居中顯示;
dependencies: flutter: sdk: flutter window_manager: ^0.2.6
// runApp之前運(yùn)行 WindowManager w = WindowManager.instance; await w.ensureInitialized(); WindowOptions windowOptions = WindowOptions( size: normalWindowSize, center: true, titleBarStyle: TitleBarStyle.hidden // 該屬性隱藏導(dǎo)航欄 ); w.waitUntilReadyToShow(windowOptions, () async { await w.setBackgroundColor(Colors.transparent); await w.show(); await w.focus(); await w.setAsFrameless(); });
- 此時(shí)會(huì)發(fā)現(xiàn)應(yīng)用打開時(shí)在左下角閃一下再居中。這是由于原生win32窗口默認(rèn)是左上角顯示,而后在flutter通過(guò)插件才居中;
- 處理方式建議在原生代碼中先把窗口設(shè)為默認(rèn)不顯示,通過(guò)上面的window_manager.show()展示出來(lái);
// 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);
美化應(yīng)用窗口
通過(guò)前面的步驟,我們?cè)赼ndroid和windows平臺(tái)上都得到了一個(gè)安全透明的窗口,接下來(lái)的修飾Flutter就可以為所欲為了。
- 窗口陰影、圓角
上面介紹過(guò)在MaterialApp外套有盒子控件,直接在Container內(nèi)加入陰影和圓角即可,不過(guò)Android和桌面平臺(tái)還是需要區(qū)分下的;
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, ), ); } }
- 自定義導(dǎo)航欄
回歸Scaffold的AppBar配置,再加上導(dǎo)航拖拽窗口事件(僅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(), ); }
到這里多平臺(tái)的窗口就配置好了,接下來(lái)可以愉快的編寫頁(yè)面啦。
可能有些小伙伴會(huì)說(shuō):窗口的效果本就應(yīng)該由原生去寫,為啥要讓Flutter去做這么多的事情?
答案很簡(jiǎn)單:
跨平臺(tái)! 要跨平臺(tái)就勢(shì)必需要繞一些,通過(guò)這種方式你會(huì)發(fā)現(xiàn)任何平臺(tái)的應(yīng)用,都可以得到相同效果的窗口,而代碼只需要Flutter寫一次,這才是Flutter存在的真正意義。
二、windows平臺(tái)特定交互
在開發(fā)windows的過(guò)程中,我發(fā)現(xiàn)跟移動(dòng)app最大的不同在于:桌面應(yīng)用需要頻繁的去與系統(tǒng)做一些交互。
注冊(cè)表操作
應(yīng)用開發(fā)過(guò)程中,經(jīng)常需要通過(guò)注冊(cè)表來(lái)做數(shù)據(jù)存儲(chǔ);在pub上也有一個(gè)庫(kù)提供這個(gè)能力,但是我沒(méi)有使用,因?yàn)?strong>dart已經(jīng)提供了win32相關(guān)的接口,我認(rèn)為這個(gè)基礎(chǔ)的能力沒(méi)必要引用多一個(gè)庫(kù),所以手?jǐn)]了一個(gè)工具類來(lái)操作注冊(cè)表。(值得注意的是部分注冊(cè)表的操作是需要管理員權(quán)限的,所以應(yīng)用提權(quán)要做好)
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 { /// 根據(jù)鍵名獲取注冊(cè)表的值 static String? getRegeditForKey(String regPath, String key, {int hKeyValue = HKEY_LOCAL_MACHINE}) { var res = getRegedit(regPath, hKeyValue: hKeyValue); return res[key]; } /// 設(shè)置注冊(cè)表值 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); } } /// 獲取注冊(cè)表所有子項(xiàng) 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; } /// 刪除注冊(cè)表的子項(xiàng) 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); } } /// 根據(jù)項(xiàng)的路徑獲取所有值 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); } } }
執(zhí)行控制臺(tái)指令
windows上,我們可以通過(guò)cmd指令做所有事情,dart也提供了這種能力。我們可以通過(guò)io庫(kù)中的Progress類來(lái)運(yùn)行指令。如:幫助用戶打開網(wǎng)絡(luò)連接。
Process.start('ncpa.cpl', [],runInShell: true);
剛接觸桌面開發(fā)的小伙伴,真的很需要這個(gè)知識(shí)點(diǎn)。
實(shí)現(xiàn)應(yīng)用單例
應(yīng)用單例是windows需要特殊處理,android默認(rèn)是單例的。而windows如果不作處理,每次點(diǎn)擊都會(huì)重新運(yùn)行一個(gè)應(yīng)用進(jìn)程,這顯然不合理。Flutter可以通過(guò)windows_single_instance插件來(lái)實(shí)現(xiàn)單例。在runApp之前執(zhí)行下這個(gè)方法,重復(fù)點(diǎn)擊時(shí)會(huì)讓用戶獲得焦點(diǎn)置頂,而不是多開一個(gè)應(yīng)用。
/// windows設(shè)置單實(shí)例啟動(dòng) static setSingleInstance(List<String> args) async { await WindowsSingleInstance.ensureSingleInstance(args, "desktop_open", onSecondWindow: (args) async { // 喚起并聚焦 if (await windowManager.isMinimized()) await windowManager.restore(); windowManager.focus(); }); }
三、桌面應(yīng)用的交互習(xí)慣
按鈕點(diǎn)擊態(tài)
按鈕點(diǎn)擊交互的狀態(tài),其實(shí)在移動(dòng)端也存在。但不同的是移動(dòng)端的按鈕基本上水波紋的效果就能滿足用戶使用,但是桌面應(yīng)用顯示區(qū)域大,而點(diǎn)擊的鼠標(biāo)卻很小,很多時(shí)候點(diǎn)擊已經(jīng)過(guò)去但水波紋根本就沒(méi)顯示出來(lái)。
正常交互是:點(diǎn)擊按鈕馬上響應(yīng)點(diǎn)擊態(tài)的顏色(文本和背景都能編),松開恢復(fù)。
TextButton( clipBehavior: Clip.antiAliasWithSaveLayer, style: ButtonStyle( animationDuration: Duration.zero, // 動(dòng)畫延時(shí)設(shè)置為0 visualDensity: VisualDensity.compact, overlayColor: MaterialStateProperty.all(Colors.transparent), padding: MaterialStateProperty.all(EdgeInsets.zero), textStyle: MaterialStateProperty.all(Theme.of(context).textTheme.subtitle1), // 按鈕按下的時(shí)候的前景色,會(huì)讓文本的顏色按下時(shí)變?yōu)榘咨? foregroundColor: MaterialStateProperty.resolveWith((states) { return states.contains(MaterialState.pressed) ? Colors.white : Theme.of(context).toggleableActiveColor; }), // 按鈕按下的時(shí)候的背景色,會(huì)讓背景按下時(shí)變?yōu)樗{(lán)色 backgroundColor: MaterialStateProperty.resolveWith((states) { return states.contains(MaterialState.pressed) ? Theme.of(context).toggleableActiveColor : null; }), ), onPressed: null, child: XXX), )
獲取應(yīng)用啟動(dòng)參數(shù)
由于我們的桌面設(shè)備升級(jí)自研的整機(jī),因此在開發(fā)過(guò)程經(jīng)常遇到其他軟件要喚起Flutter應(yīng)用的需求。那么如何喚起,又如何拿到喚起參數(shù)呢?
1. windows:其他應(yīng)用通過(guò)Procress.start啟動(dòng).exe即可運(yùn)行Flutter的軟件;傳參也非常簡(jiǎn)單,直接.exe后面帶參數(shù),多個(gè)參數(shù)使用空格隔開,然后再Flutter main函數(shù)中的args就能拿到參數(shù)的列表,非常方便。
其實(shí)cmd執(zhí)行的參數(shù),是被win32Window接收了,只是Flutter幫我們做了這層轉(zhuǎn)換,通過(guò)engine傳遞給main函數(shù),而Android就沒(méi)那么方便了。
2. Android:Android原生啟動(dòng)應(yīng)用是通過(guò)Intent對(duì)應(yīng)包名下的Activity,然后再Activity中通過(guò)Intent.getExtra可以拿到參數(shù)。我們都知道Android平臺(tái)下Flutter只有一個(gè)Activity,因此做法是先在MainActivity中拿到Intent的參數(shù),然后建立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函數(shù)中通過(guò)Method Channel的方式取到MainActivity中存儲(chǔ)的參數(shù),繞多了一層鏈路。 ```dart const platform = MethodChannel('app.open.shared.data'); String? sharedData = await platform.invokeMethod('getSharedText'); if (sharedData == null) return null; return jsonDecode(sharedData); ```
四、寫在最后
通過(guò)上面這么多的實(shí)現(xiàn),我們已經(jīng)完全把一個(gè)應(yīng)用窗體結(jié)構(gòu)搭建起來(lái)了。長(zhǎng)篇幅的實(shí)戰(zhàn)記錄,希望可以切實(shí)的幫助到大家??傮w來(lái)說(shuō),桌面開發(fā)雖然還有很多缺陷,但是能用,性能尚佳,跨平臺(tái)降低成本。
以上就是Android開發(fā)Flutter 桌面應(yīng)用窗口化實(shí)戰(zhàn)示例的詳細(xì)內(nèi)容,更多關(guān)于Android Flutter 桌面應(yīng)用窗口化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android自定義view之太極圖的實(shí)現(xiàn)教程
這篇文章主要給大家介紹了關(guān)于Android自定義view之太極圖的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Android冷啟動(dòng)優(yōu)化的3個(gè)小案例分享
為了提高App的冷啟動(dòng)耗時(shí),除了在常規(guī)的業(yè)務(wù)側(cè)進(jìn)行耗時(shí)代碼優(yōu)化之外,為了進(jìn)一步縮短啟動(dòng)耗時(shí),需要在純技術(shù)測(cè)做一些優(yōu)化探索,本期我們從類預(yù)加載、Retrofit 、ARouter方面進(jìn)行了進(jìn)一步的優(yōu)化,感興趣的同學(xué)跟著小編一起來(lái)看看吧2023-07-07PowerManagerService之喚醒鎖的使用獲取創(chuàng)建示例解析
這篇文章主要為大家介紹了PowerManagerService之喚醒鎖的使用獲取創(chuàng)建示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10Android中的指紋識(shí)別demo開發(fā)實(shí)例
這篇文章主要介紹了Android中的指紋識(shí)別demo開發(fā)實(shí)例的相關(guān)知識(shí),非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09詳解android在mob平臺(tái)實(shí)現(xiàn)qq登陸和分享
這篇文章主要介紹了詳解android在mob平臺(tái)實(shí)現(xiàn)qq登陸和分享,對(duì)接入第三方平臺(tái)SDK感興趣的同學(xué)們,可以參考下2021-04-04Android UI設(shè)計(jì)系列之自定義Dialog實(shí)現(xiàn)各種風(fēng)格的對(duì)話框效果(7)
這篇文章主要介紹了Android UI設(shè)計(jì)系列之自定義Dialog實(shí)現(xiàn)各種風(fēng)格的對(duì)話框效果,具有一定的實(shí)用性和參考價(jià)值,感興趣的小伙伴們可以參考一下2016-06-06自定義GridView并且實(shí)現(xiàn)拖拽(附源碼)
本文實(shí)現(xiàn)了GridView的拖拽功能,原理很簡(jiǎn)單只是在交換位置上記錄了X軸的相關(guān)坐標(biāo),計(jì)算了X軸的相關(guān)變量,實(shí)例代碼如下,感興趣的額朋友可以參考下哈2013-06-06Android自定義View Flyme6的Viewpager指示器
這篇文章主要為大家詳細(xì)介紹了Android自定義View Flyme6的Viewpager指示器,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01android實(shí)現(xiàn)滾動(dòng)文本效果
這篇文章主要為大家詳細(xì)介紹了android實(shí)現(xiàn)滾動(dòng)文本效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05