探索Flutter路由管理新姿势(flutter路由返回自动刷新)
cac55 2024-09-20 12:51 24 浏览 0 评论
1、Flutter路由
1.1、路由分类
几乎所有 UI 框架开发出来的大型应用都是由数十甚至数百个页面组成。在 Android 项目中,每个页面都被承载在一个 Activity 中,因此一个 Activity 可以被认为是 Android 应用中的一个页面。在 Flutter 中,每个页面都对应一个 Route 对象,需要注意的是,弹窗也是 Route 的一种表现形式。Flutter 通过 Navigator 以栈结构来管理所有打开页面对应的 Route 对象。当一个页面打开时,对应的 Route 对象就被压入栈中;当一个页面关闭时,对应的 Route 对象就会从栈中弹出。
虽然 Flutter 中页面和弹窗都是用 Route 表示,但是两者之间的交互和表现形式存在明显的区别。为了保证清晰的代码结构和良好的可维护性,Flutter 按照表现形式通过类继承的方式对 Route 相关类进行了划分,具体如下:
1)OverlayRoute
在 Flutter 应用中,Overlay 扮演了至关重要的角色,它负责在视图上正确地显示页面和弹窗。要实现这一目的,我们需要将 Route 加载到 Overlay 中,而 OverlayRoute 就是用于实现这一目的的重要类之一。在 Flutter 应用中,通常会使用 MaterialApp 作为根节点,而 MaterialApp 中会内嵌一个 Navigator 对象,用于管理页面的显示与隐藏。同时,Navigator 内部还嵌套了 Overlay Widget,用于显示 OverlayRoute 对象。
2)TransitionRoute
当Overlay中的Route进行切换时,TransitionRoute是一个提供Route切换动画效果的抽象类,通过配合使用SlideTransition、FadeTransition等Widget,控制页面打开或关闭时的动画。
3)ModalRoute
保证所有的手势事件都被当前的ModalRoute处理,其底层的Route无法感知任何手势事件。
4)PageRoute
对应Flutter中的页面,适配各平台的页面交互特性,如iOS系统页面可侧滑退出。
5)DialogRoute
对应Flutter中的弹窗,支持点击弹窗外部区域退出等特性。
1.2、简单使用
在Flutter开发中,可以通过以下三种方式打开页面,使用示例如下:
1)组件路由
import 'package:flutter/material.dart';
void main() {
runApp(Nav2App());
}
class Nav2App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('open Details'),
onPressed: () {
Navigator.push( //1、打开详情页
context,
MaterialPageRoute(builder: (context) {
return DetailScreen();
}),
);
},
),
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('Back'),
onPressed: () {
Navigator.pop(context); //2、关闭详情页
},
),
),
);
}
}
当 push() 被调用时,DetailScreen 页面被放置在 HomeScreen 页面的前面,此时与用户交互的页面是最顶层的DetailScreen页面,效果如下:
2)命名路由
import 'package:flutter/material.dart';
void main() {
runApp(Nav2App());
}
class Nav2App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp( //1、注册路由表
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailScreen(),
},
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('open Details'),
onPressed: () {
Navigator.pushNamed(
context,
'/details', //2、通过路由表中路由名称,打开相应页面
);
},
),
),
);
}
}
在使用命名路由前,需要提前以name- Page键值对的形式将路由表注册到Navigator中。在进行路由跳转的时候,通过name即可打开路由表中对应的页面。相比组件路由的方式,使用命名路由打开页面代码简洁了不少。
3)生成路由
import 'package:flutter/material.dart';
void main() {
runApp(Nav2App());
}
class Nav2App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: (settings) { //2、通过路由名称,生成对应路由对象
if (settings.name == '/') {
return MaterialPageRoute(builder: (context) => HomeScreen());
}
var uri = Uri.parse(settings.name);
if (uri.pathSegments.length == 2 &&
uri.pathSegments.first == 'details') {
var id = uri.pathSegments[1];
return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
}
return MaterialPageRoute(builder: (context) => UnknownScreen());
},
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('open Details'),
onPressed: () {
Navigator.pushNamed(
context,
'/details/1', //1、传入路由名称,打开页面
);
},
),
),
);
}
}
虽看上去生成路由与命名路由打开页面的方式一样,都是通过路由标识字符串的形式打开对应的页面。但命名路由需要提前将路由表提前注册到Navigator中,而生成路由在页面进行跳转时,临时解析路由标识字符串,并确定需要打开的页面。
在使用上,生成路由要比命名路由灵活,但是后期代码维护成本,代码结构清晰度却远不如命名路由。
通过对以上三种路由跳转方式的说明,命名路由在使用的简洁度以及代码结构清晰度上,更愿意被大部分项目所接受使用。
2、已有项目路由管理现状
项目大多采用命名路由的方式对路由进行统一管理,但是由于命名路由需要提前将路由表注册到Navigator中,所以每当新增一个页面,就需要往路由表中添加一个路由配置,例如:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
...
initialRoute: '/home',
routes: {
'/home': (context) => HomePage(), //路由注册
'/a': (context) => APage(),
'/b': (context) => BPage(),
},
);
}
}
3、路由管理方案设计
3.1、现有项目路由管理存在哪些问题?
通过分析上面路由注册配置代码,如果页面想通过命名路由的方式打开,则页面需要提前注册到Navigator中。随着项目规模变大,页面逐步增加,在MyApp中注册路由,问题也愈发明显:
耦合度:随着页面不断新增,路由注册代码也随之追加,MyApp类变得越来越臃肿,类中充斥着大量对其他类的引用。
模块化:由于路由注册逻辑统一在MyApp类中,不同模块的路由不能单独管理控制,完全的扁平化:
3.2、重塑项目路由管理
针对项目路由管理高耦合、无法模块化的问题,假设我们项目路由结构做出如下调整:
1、将项目中的所有页面按照模块进行划分,每个模块内部完成页面注册,使得不同模块的路由管理相互独立,方便开发人员进行单独的模块开发和维护。
2、通过App下的Navigator完成模块注册,方便地实现不同模块之间的页面跳转,使得Flutter应用程序的结构更加清晰,易于扩展和维护。
3.3、mixin机制说明
Flutter中的mixin机制是一种代码重用的技术,可以帮助开发者在不使用继承的情况下将代码的功能注入到其他类中。mixin可以看作是一种将一组函数、属性和其他代码注入到类中的方式,以实现代码复用。
下面是一个简单的例子,其中我们创建了一个名为 Runner 的 mixin,它包含了一个run()函数:
mixin Runner {
void run() {
print("I'm Running!");
}
}
然后我们定义了一个类Person,它使用了Runner mixin:
class Person with Runner {
String name;
Person(this.name);
}
现在,我们可以使用Person类的实例并调用其run()函数,因为Person类已经将 Runner mixin 中的函数注入到了自身中:
void main() {
var Person = Person("xiaoying");
person.run(); // Output: "I'm Running!"
}
需要注意的是,mixin机制并不是继承,而是一种注入代码的方式。因此,它可以避免一些继承带来的问题,比如多重继承的复杂性。同时,mixin机制也使得代码更加灵活,可以组合不同的功能,以满足不同的需求。
3.4、flutter-mixin-router 介绍
源码WidgetsFlutterBinding通过mixin机制来管理和协调不同模块工作,使得Flutter框架在不同平台下表现更为稳定和高效。同样,我们也可以利用这一机制,将不同的路由管理模块进行组合,并注册到App的Navigator中,实现不同模块之间的路由管理相互独立,具体代码如下:
1)创建模块管理基类
class MixinRouterContainer {
Map<String, WidgetBuilder> installRouters() => {};
Future<T?>? openPage<T>(BuildContext context, String pageName ...}) {
...
return Navigator.pushNamed(context, pageName, arguments: args);
...
}
}
在基类中仅定义了两个方法:
installRouters: 注册该模块下的所有页面。
openPage:打开该模块下注册的页面
2)页面注册到模块中
//设置模块
mixin SettingRouteContainer on MixinRouterContainer {
@override
Map<String, WidgetBuilder> installRouters() {
Map<String, WidgetBuilder> originRoutes = super.installRouters();
Map<String, WidgetBuilder> newRoutes = {};
newRoutes['/setting_a'] = (context) => APage(); //注册A页面
newRoutes['/setting_b'] = (context) => BPage(); //注册B页面
newRoutes.addAll(originRoutes);
return newRoutes;
}
}
//大厅模块
mixin HomeRouteContainer on MixinRouterContainer {
@override
Map<String, WidgetBuilder> installRouters() {
Map<String, WidgetBuilder> originRoutes = super.installRouters();
Map<String, WidgetBuilder> newRoutes = {};
newRoutes['/home'] = (context) => HomePage(); //注册大厅页面
newRoutes.addAll(originRoutes);
return newRoutes;
}
}
不同模块的路由管理都通过mixin RouterContainer,并将模块内部的页面注册到其中。例如,HomeRouteContainer 将 HomePage 添加到自己的路由表中,SettingRouteContainer 管理 SettingA 和 SettingB 两个页面。不同的路由模块都能够独立管理自己模块内的页面,从而实现了路由模块的高度解耦。
3)模块注册到App中
//1、创建总路由管理类,并通过mixin机制将各个路由模块进行粘合,即:
// AppRouteContainer 将 HomeRouteContainer 和 SettingRouteContainer... 组装
class AppRouteContainer extends MixinRouterContainer with HomeRouteContainer, SettingRouteContainer {
AppRouteContainer._();
static AppRouteContainer _instance = AppRouteContainer._();
static AppRouteContainer get share => _instance;
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
...
initialRoute: '/home',
// 2、将总路由注册到App中
routes: AppRouteContainer.share.installRouters(),
);
}
}
通过创建一个总路由管理类,利用mixin机制将不同的路由模块组合起来,然后将其注册到App的Navigator中。在代码中,我们可以看到AppRouteContainer通过组合大厅路由模块(HomeRouteContainer)和设置路由模块(SettingRouteContainer),实现了路由模块的高度解耦。为了方便在项目中使用总路由管理类,我们将其改写为单例模式。在使用总路由管理类时,只需要了解以下两个方法:
路由表注册:AppRouteContainer.share.installRouters()
页面跳转:AppRouteContainer.share.openPage(context, '/setting_a')
4、路由管理方案扩充
4.1、如何进行路由拦截?
在项目开发中,路由拦截是一个常见的需求。例如,当用户尚未登录时,如果想打开个人主页,需要拦截这一过程并将用户重定向到登录页面。
为了解决这个问题,可以在现有的路由管理模块的基类(MixinRouterContainer)上再封装,添加拦截路由表并重写路由跳转过程来实现。
//定义路由拦截回调函数
typedef MixinRouteInterceptor = bool Function(BuildContext context, String pageName, ...);
//对MixinRouterContainer进行封装
class MixinRouterInterceptContainer extends MixinRouterContainer {
//添加拦截路由表逻辑
final Map<String, MixinRouteInterceptor> _routeInterceptorTable = {};
void registerRouteInterceptor(String pageName, MixinRouteInterceptor interceptor) {
_routeInterceptorTable[pageName] = interceptor;
}
void unRegisterRouteInterceptor(String pageName) {
_routeInterceptorTable.remove(pageName);
}
//重写路由跳转过程
@override
Future<T?>? openPage<T>(BuildContext context, String pageName,...) {
//拦截跳转
if (!_routeInterceptorTable.containsKey(pageName)) {
return super.openPage(context,pageName,...);
}
MixinRouteInterceptor interceptor = _routeInterceptorTable[pageName]!;
bool needIntercept = interceptor.call(context,pageName,...);
if (needIntercept) {
return Future.value(null);
} else {
return super.openPage(context,pageName,...);
}
}
}
该类增加了拦截路由配置的注册和反注册逻辑,在进行页面跳转时,判断当前路由是否能被拦截,如果是,则会拦截后续的页面跳转逻辑,并执行拦截相关的处理工作,否则将会继续进行页面跳转。
下面我们来改造大厅路由管理模块,使之能处理登录拦截,代码如下:
mixin HomeRouteContainer on MixinRouterInterceptContainer {
@override
Map<String, WidgetBuilder> installRouters() {
//注册拦截路由表
registerRouteInterceptor('/home', (...) => if(!isLogin) openLoginPage());
Map<String, WidgetBuilder> originRoutes = super.installRouters();
Map<String, WidgetBuilder> newRoutes = {};
newRoutes['/home'] = (context) => HomePage();
newRoutes.addAll(originRoutes);
return newRoutes;
}
}
4.2、怎么实现Url统跳?
对于很多项目来说,为了能够通过外链打开对应的页面,常常采用URL统一跳转。为了实现这个功能,只需要对现有的总路由管理类AppRouteContainer进行扩展,代理默认的页面打开行为,实现URL的解析。
class AppRouteContainer extends MixinRouterContainerwith HomeRouteContainer, SettingRouteContainer {
...
Future<T?>? urlToPage<T>(BuildContext context, String urlStr, ...) {
//1、解析URL
Uri? url = Uri.tryParse(urlStr);
if (url == null) return Future.error('parse url fail');
Map<String, String> args = {};
args.addAll(url.queryParameters);
args['_url'] = urlStr;
String pageName = url.host;
//2.通过HOST作为路由名称,打开对应页面
super.openPage(context,'/' + pageName ...);
}
}
在项目中,可以通过添加 urlToPage(...) 方法来对 openPage(...) 方法进行封装。通过调用 AppRouteContainer.share.urlToPage(...) 并传入 URL 字符串,该方法会对传入的URL进行解析,并提取出 HOST 和参数。HOST 作为路由名称,并将参数传递给 openPage 方法,从而打开对应的页面。这样就可以实现通过 URL 统一跳转到对应的页面。
5、路由管理方案增效
5.1、回顾与思考
经过以上的路由改造,是否就可以说路由的问题解决了呢?然而,仔细回顾之前的改造过程,我们会发现还存在一些问题:
页面注册:仍需要手动注册新的页面到对应的模块类中。
路由模块管理:还需要手动创建并维护不同的路由管理模块,如 HomeRouteContainer、SettingRouteContainer
5.2、站在巨人的肩膀上前行
客户端原生项目中,ARouter通过注解生成路由管理文件可以省去手动创建和维护不同的路由管理模块的麻烦。Flutter可以借鉴这种方式,使用注解来自动生成对应的路由模块管理文件。这样做的好处是可以提高开发效率,降低出错率,同时也可以避免代码冗余和重复的工作。另外,注解方式可以让开发者更加专注于业务逻辑的开发,而不用花费太多精力在路由管理的维护上。示例代码如下:
1)注解子路由表
const String HOME_ROUTE_TABLE = 'HomeRouteTable';
const String SETTING_ROUTE_TABLE = 'SettingsRouteTable';
//tDescription: 仅仅作为生成类的注释
@RouterTableList(
tableList: [
RouterTable(tName: HOME_ROUTE_TABLE, tDescription: '大厅路由模块'),
RouterTable(tName: SETTING_ROUTE_TABLE, tDescription: '设置路由模块'),
],
)
class AppRouteContainer extends MixinRouterInterceptContainer
with HomeRouteTable, SettingsRouteTable {
AppRouteContainer._();
static AppRouteContainer _instance = AppRouteContainer._();
static AppRouteContainer get share => _instance;
}
2)注解普通路由
//将页面注册到 SettingsRouteTable 模块中,并指定页面的路由名称
@MixinRoute(tName: SETTING_ROUTE_TABLE, path: '/setting_a')
class APage extends StatelessWidget {
const APage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Text('APage'),
);
}
}
3)注解拦截路由
@MixinRoute(tName: SETTING_ROUTE_TABLE, path: '/setting_b')
class BPage extends StatelessWidget {
const BPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Text('BPage'),
);
}
}
//注解拦截路由函数
@MixinInterceptRoute(tName: SETTING_ROUTE_TABLE, path: '/setting_b')
bool interceptorMinePage(context, pageName, pushType, {arguments, predicate}) {
print('toLogin');
return true;
}
编写一个顶层函数,并通过 MixinInterceptRoute 进行注解,该函数签名具体如下:
bool Function(BuildContext context, String pageName, String pushType, {Map<String, dynamic>? arguments, bool Function()? predicate});
- BuildContext context:BuildContext对象,表示当前BuildContext。
- String pageName:字符串类型,表示需要拦截的页面名称。
- String pushType:字符串类型,表示跳转类型,如push、pushNamed、pushReplacement等。
- Map<String, dynamic>? arguments:可选的Map类型,表示传递给目标页面的参数。
- bool Function()? predicate:可选的bool类型回调函数,控制页面打开策略。
- 函数返回结果代表本次拦截是否消费原本的页面跳转,如果返回true,则继续执行后续页面打开操作,否则终止后续跳转逻辑。
6、项目集成
在项目的pubspec.yaml中添加依赖,即可开启注解路由之旅
dependencies:
flutter:
sdk: flutter
flutter_mixin_router: ^1.0.0 # 添加路由模块管理基类
flutter_mixin_router_ann: 1.0.0 # 添加注解类
dev_dependencies:
build_runner: 2.1.8 # 添加依赖
flutter_mixin_router_gen: 1.0.1 # 添加代码生成工具库
在项目页面上添加对应的注解后,执行以下命令生成对应的路由代码
# 清除增量编译缓存
flutter packages pub run build_runner clean
# 重新生成代码
flutter packages pub run build_runner build --delete-conflicting-outputs
7、实际项目收益
1)项目代码结构的优化
路由注册相关的代码可以被分模块管理,这使得项目中的App入口类代码行数从1500行缩减到200行以内,项目的代码结构更加清晰,降低了代码的维护难度,提高代码的可读性和可维护性。
2)路由代码冲突的减少
不同的开发人员负责开发不同的模块,如果路由相关的代码没有被分模块管理,那么就容易出现代码冲突的问题,在使用 flutter_mixin_router 后,路由相关的代码合并冲突几乎不再发生,提高代码的稳定性,从而降低了项目出错的概率。
3)开发效率的提升
使用注解可以省去编写路由注册相关的代码,提高了代码的简洁性。由于开发人员可以更加专注于业务逻辑的实现,从而提高了开发效率。
8、总结与展望
使用 flutter_mixin_router 可以让开发人员更专注于业务逻辑的实现,快速地迭代开发,提高项目的上线速度;模块化的路由管理,能够有效地应对项目规模的增长,并保持代码的一致性和可维护性。
希望该框架的持续维护和更新也能够为团队提供更多的功能,满足不断增长的业务需求。
作者:杨浪
来源:微信公众号:映客技术
出处:https://mp.weixin.qq.com/s/Yiq140plcoOKsgwL01Hhzw
相关推荐
- iphone6自定义铃声设置流程(iphone6怎么自定义来电铃声)
-
苹果iphone6自定义铃声如何设置,iphone6自定义铃声怎么设置,iphone6自定义铃声设置教程,下面小编给大家分享一下。设置自定义铃声首先要自己制作或者找到一个铃声音频。1、在电脑上面把已经...
- iphone手机三分钟更换铃声,无需电脑直接手机操作
-
iPhone怎么在手机上换铃声?无需电脑!1分钟教你给苹果手机换铃声众所周知,苹果手机的ios系统是比较封闭的,封闭系统就会给我们带来一些不便,这里要说的就是苹果手机更换个性化铃声就比较麻烦,因为io...
- iPhone手机个性铃声设置详细教程(iphone个性铃声怎么设置)
-
iPhone现在已成街机了。朋友聚在一起的时候,是不是总有种以为是自己手机响了的赶脚。那么,小编今天跟大家分享一下iPhone怎么换铃声?,让你轻松设置属于你的个性铃声。1:电脑端安装iTools,安...
- iPhone也能用自己喜欢的铃声了,2分钟包搞定!
-
听到超好听的铃声,怎样才能放进iPhone里?这貌似是一道千古难题。90%的iPhone小白:听到这么爽脆带感的iPhone铃声,我要用!我要用!我要用!视频然而打开iTunes发现,脑子一片空白……...
- 苹果iOS 26隐藏新铃声曝光:强调“玻璃质感”
-
IT之家6月20日消息,苹果在iOS26中隐藏了一个新的铃声,这是现有默认铃声“Reflection”的改版。“Reflection”自2017年iPhoneX问世以来一直是系...
- 苹果ios14充电提示音怎么设置 iPhone手机修改充电提示音教程
-
ios14充电提示音最近很火爆,大家都想要知道苹果设置充电提示音的方法,个性化的设置非常吸引大家,小编也会在这里教大家ios14苹果充电提示音的设置,操作的流程会直接分享在下方,各位玩家们都能一起来看...
- iPhone苹果16自定义来电铃声攻略(ios自定义铃声怎么设置)
-
在智能手机的个性化设置中,更改来电铃声是展现自我风格的一种方式。对于使用iPhone苹果16的用户来说,设置一个独特的来电铃声,不仅能提升接听体验,还能让日常生活更加多彩。以下是如何为iPhone苹果...
- 未越狱iPhone用户自定义来电铃声和短信铃声的教程
-
其实自定义来电铃声和短信铃声的过程没有什么区别,但要注意,来电铃声的播放时间不能超过40秒,短信铃声的播放时间不能超过30秒,这也就是说长度为30-40秒的仅在iPhone的来电铃声中出现,少于30秒...
- 苹果手机怎么设置闹钟铃声?更改为歌曲铃声,亲测有效
-
很不是有很多小伙伴每天早上都被苹果手机刺耳的“雷达”闹钟铃声给吵醒呢?想要更换一个舒缓的闹钟铃声,却发现自己鼓捣半天却无法更换喜欢的歌曲闹钟铃声。苹果手机怎么设置闹钟铃声?下面小编就来分享如何将苹果手...
- 独家教程:iPhone手机铃声制作与更换,一般人我不告诉他
-
今天刚好自己作铃声。。想到吧里好多人都会问怎么制作铃声。于是顺便截图发吧里。虽然百度一下铃声制作已经泛滥。但是还是会一直有小白问。所以这里会详细图文说明。先介绍小白式铃声制作。http://www....
- 苹果用户iTunes自制自定义铃声教程
-
怎么制作iphone6铃声,如何使用iTunes剪切音乐自制铃声,新版iTunes怎么自制铃声,相信很多苹果用户都想使用一些个性化铃声。那么就学习一下这个方法吧。1、在电脑上面打开iTunes,选择编...
- 轻松学会!苹果手机怎么设置铃声来电铃声(3个方法)
-
在日常生活中,手机的铃声扮演着非常重要的角色,不仅是接收来电的提示,更是展示个人品位和风格的方式之一。而对于苹果手机用户来说,定制来电铃声可以让手机更具个性化,并且让您在繁忙的环境中更容易识别重要的来...
- iPhone13怎么设置来电铃声?苹果13自定义铃声操作教程
-
我们知道,苹果iOS系统是相对封闭的,在很多功能设置上与安卓机大有不同。就如来电铃声,很多苹果用户就不喜欢使用默认的那些来电铃声,想要自定义铃声,却不知如何操作。最近,新购入iPhone13系列机型的...
- iPhone 免费铃声:iOS 26 终于让设置变得更简单
-
苹果终于开窍了——省去了最烦人的繁琐步骤。iOS26终结了多年来困扰iPhone用户的麻烦操作:现在无需通过GarageBand应用,仅需轻点几下就能设置自定义铃声。完全免费、直接操作且极...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- iphone6自定义铃声设置流程(iphone6怎么自定义来电铃声)
- iphone手机三分钟更换铃声,无需电脑直接手机操作
- iPhone手机个性铃声设置详细教程(iphone个性铃声怎么设置)
- iPhone也能用自己喜欢的铃声了,2分钟包搞定!
- 苹果iOS 26隐藏新铃声曝光:强调“玻璃质感”
- 苹果ios14充电提示音怎么设置 iPhone手机修改充电提示音教程
- 苹果iphone手机直接设置铃声教程(ios怎么直接设置铃声)
- iPhone苹果16自定义来电铃声攻略(ios自定义铃声怎么设置)
- 未越狱iPhone用户自定义来电铃声和短信铃声的教程
- 苹果手机怎么设置闹钟铃声?更改为歌曲铃声,亲测有效
- 标签列表
-
- 如何绘制折线图 (52)
- javaabstract (48)
- 新浪微博头像 (53)
- grub4dos (66)
- s扫描器 (51)
- httpfile dll (48)
- ps实例教程 (55)
- taskmgr (51)
- s spline (61)
- vnc远程控制 (47)
- 数据丢失 (47)
- wbem (57)
- flac文件 (72)
- 网页制作基础教程 (53)
- 镜像文件刻录 (61)
- ug5 0软件免费下载 (78)
- debian下载 (53)
- ubuntu10 04 (60)
- web qq登录 (59)
- 笔记本变成无线路由 (52)
- flash player 11 4 (50)
- 右键菜单清理 (78)
- cuteftp 注册码 (57)
- ospf协议 (53)
- ms17 010 下载 (60)