赞
踩
本篇文章将深入探讨HarmonyOS、Flutter和Vue的基础组件与基础方法。我们将从语言、框架和UI设计理念等方面入手,分析它们的共同之处和差异。HarmonyOS采用ArkTS作为前端开发语言,基于TypeScript演进,其UI则由方舟开发框架ArkUI实现。与之类似,Flutter和Vue也采用声明式编程风格,但它们在组件布局和属性设置上有所不同。以此文章,抛砖引玉,以我个人的视角说明它们的特点.欢迎指正和批评
HarmonyOS前端开发语言为 ArkTS,基于 TypeScript
演进而来。因此,熟悉 TypeScript
是必备的。
HarmonyOS的UI是通过方舟开发框架 ArkUI 实现的,其中又分为Stage模型和FA模型。主要应用开发采用Stage模型。本文将重点介绍Stage模型。
HarmonyOS、Flutter、VUE都采用 声明式编程 风格。
ArkTS 和 Flutter 都是通过组件进行布局,布局和逻辑交叉在一起,而 VUE 则明确区分了HTML、JavaScript、CSS等模块。
Flutter的设计理念为万物皆为Widget,设置组件的属性(例如:width
、height
、margin
、padding
、onClick
)都是需要使用对应的Widget实现。
而AskTS通过抽取组件间公共属性的方式可以直接设置属性,相较于Flutter,设置属性方便了很多,也稍微缓解了组件嵌套问题,
但由于两者的特性,嵌套问题一定会存在。
因此,强烈建议在编写AskTS UI时,多抽象业务组件以提高代码的可读性。
此外,相对于Vue和Flutter有一个好大的改善就是DEV Studio还新增了UI预览器,可直接预览UI布局情况,无需通过手机或模拟器查看。
另外,我非常不理解,为何在构建布局方法build
时要去掉return
关键词,这一点确实令人困惑。
关于DevStudio的一些注意事项(MAC):
在学习前端语言时,个人建议从以下几个方面入手:
了解以上内容的实现方式,基本上就能掌握前端基础开发所需的技能。
我将以这几个方面来说明三种技术如何完成以上功能
my-vue-project/ │ ├── public/ │ ├── index.html # 主HTML文件 │ └── ... │ ├── src/ │ ├── assets/ # 静态资源如图片、样式等 │ │ └── ... │ │ │ ├── components/ # 组件文件夹 │ │ └── ... │ │ │ ├── views/ # 视图文件夹 │ │ └── ... │ │ │ ├── App.vue # 根组件 │ └── main.js # 入口文件 │ ├── babel.config.js # Babel配置文件 ├── package.json # 项目配置文件 ├── vue.config.js # Vue CLI 配置文件
flutter_project/ │ ├── android/ # Android项目文件夹 │ └── ... │ ├── ios/ # iOS项目文件夹 │ └── ... │ ├── lib/ # Dart代码文件夹 │ ├── main.dart # 应用程序入口文件 │ └── ... │ ├── test/ # 测试文件夹 │ └── ... │ ├── assets/ # 静态资源文件夹 │ └── ... │ ├── pubspec.yaml # 依赖管理和项目配置文件 └── ...
ArkTS(Stage模型) harmony_project/ │ ├── app.json5 # 应用的全局配置信息 ├── entry/ # HarmonyOS工程模块,编译构建生成一个HAP包 │ └── src/ # 在entry中的src文件夹 │ ├── main/ # 主要代码 │ │ └── ets/ # 用于存放ArkTS源码 │ │ ├── entryability/ # 应用/服务的入口 │ │ └── pages/ # 应用/服务包含的页面 │ └── resources/ # 用于存放应用/服务所用到的资源文件 │ └── module.json5 # Stage模型模块配置文件 ├── build-profile.json5 # 当前的模块信息、编译信息配置项 ├── hvigorfile.ts # 模块级编译构建任务脚本 ├── oh_modules/ # 用于存放三方库依赖信息 └── build-profile.json5 # 应用级配置信息
资源目录:
/src/assets/images/...
资源使用
<template>
<div>
<img src="../assets/logo.png" alt="Logo">
</div>
</template>
图片多分辨率适配:
资源存放方式:
/src/assets/images/user_bg@2x.png
/src/assets/images/user_bg@3x.png
资源使用:
step1:定义mixin.scss文件: /* 根据设备的像素密度选择合适的图片资源 */ @mixin bg-image($url) { background-image: url($url + "@2x.png"); @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3) { background-image: url($url + "@3x.png"); } } step2:在组件中使用mixin.scss <style lang="scss"> @import './common/css/mixin.scss'; .brand{ width:30px; height:30px; background-size:30px 30px; background-repeat:no-repeat; @include bg-image('brand'); } </style>
资源目录:
/assets/images/...
并在pubspec.yaml文件中注册(可以注册整个assets文件夹)
资源使用
Widget _buildImage(
return Image.asset("assets/images/user_bg.png",width: 100,height: 100,);
)
图片多分辨率适配:
资源存放方式:
/assets/images/...
images文件夹下放默认的图片资源, 再建立不同分辨率3x,4x的文件夹
示例:
/assets/images/user_bg.png
/assets/images/3x/user_bg.png
/assets/images/4x/user_bg.png
资源使用:
/* 正常使用即可,Flutter会根据设备的像素密度选择合适的图片资源 */
Widget _buildImage(
return Image.asset("assets/images/user_bg.png",width: 100,height: 100,);
)
资源目录
/entry/src/main/resources
资源使用
Image($r('app.media.user_bg')) // media资源的$r引用
图片多分辨率适配:
资源存放方式:
使用屏幕密度类型的限定符
包含:sdpi,mdpi,ldpi,xldpi,xxldpi,xxxldpi
/entry/src/main/resources目录中 新建 Resource Dicetory 选择屏幕密度类型.
示例:
/entry/src/main/resources/xxldpi/media/user_bg.png
/entry/src/main/resources/xxxldpi/media/user_bg.png
资源使用:
/* 正常使用即可,HarmonyOS会根据设备的像素密度选择合适的图片资源 */
Image($r('app.media.user_bg'))
页面与组件的生命周期
页面特有的生命周期(Vue Router 中的路由生命周期)
Flutter的页面也是Widget,所以都是Widget生命周期.生命周期方法包括以下几个:
setState()
时都会调用。此外,Flutter还提供了一些其他生命周期方法,如:
页面和组件的生命周期
页面特有生命周期
路由注册
定义页面
src/views
目录下创建页面
<!-- HomePage.vue --> <template> <div> <h1>Welcome to Home Page</h1> </div> </template> <script> export default { name: 'HomePage' } </script> <style scoped> /* 可选的样式 */ </style>
配置路由
router/index.js
中创建并注册路由器实例
import { createRouter, createWebHistory } from 'vue-router'; import HomePage from '../components/HomePage.vue'; const routes = [ { path: '/', component: HomePage } // 这里可以添加更多的路由配置 ]; const router = createRouter({ history: createWebHistory(), routes }); export default router;
路由注册给VUE
main.js
中将路由器配置应用到Vue实例
import { createApp } from 'vue';
import App from './App.vue';
import router from './router'; // 引入路由配置
createApp(App).use(router).mount('#app');
定义组件(与路由无关)
components目录中创建组件
使用时导入:import TestComponent from "@/components/TestComponent.vue";
导航方式
在Vue.js中,编程式导航和声明式导航是两种控制路由跳转的方式。
1. 声明式导航
<router-link to="/about">About</router-link>
2.编程式导航
this.$router.push('about')
获取路由数据
1.query
this.$route.query
2.params
this.$route.params
3.props
声明props字段
路由配置
定义页面
路径:lib/pages/HomePage.dart
//HomePage.dart
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Text('Home Page'),
),
);
}
}
路由注册到Flutter
在main.dart的MaterialApp组件 routes字段中注册.
//main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// 设置路由表
routes: {
'/': (context) => HomePage(), // 主页路由
},
initialRoute: '/', // 初始路由
);
}
}
导航方式
Navigator.pushNamed(context, '/about', arguments: 'arguments');
获取路由数据
arguments可以是任意类型
final String data = ModalRoute.of(context).settings.arguments as String;
定义组件(与路由无关)
lib目录下想建在哪里就建在哪里! 可以任性.
使用时导包即可.
路由配置
定义页面
entry/src/main/ets/entryability/pages/下创建组件
使用@Entry注解装饰。注意:一个页面有且只有一个@Entry
示例:
@Entry
@Component
struct TestPage {
}
路由注册
entry/src/main/resources/base/profile/main_pages.json文件中注册路由
DevStudio中 创建file时,选择 会自动注册
导航方式
import router from '@ohos.router';
router.pushUrl({url:"pages/AboutPage",params:{"id":"1"}}) //打开一个新的页面,覆盖在原页面智商
router.replaceUrl() //关闭当前页面,打开一个新页面
router params参数可以是任意类型,传什么类型过去接收的时就是什么类型
页面打开模式:
standard 标准模式,每次跳转同一页面都会在栈内添加一个路由,默认模式
single 单实例模式,如果栈内包含同一路由,那么就把最近的一个相同路由置为栈顶(不会删除该路由的之上的路由)
路由数据
const params = router.getParams();
console.log("router=aboutToAppear==",params["id"])
定义组件)(与路由无关
1. 使用@Component注解装饰的UI类 示例: @Component struct TestComponent { } 2.自定义组件方法,只能在当前类中使用 @Builder _buildImageSwiper() { Swiper(this.swiperController) { ForEach(this.imageList, (item) => { Image(item) .width('100%') .height('100%') }) } .height(500) .width("100%") .loop(true) .indicatorStyle({ size: 15, color: "#1A000000", selectedColor: "#6236FF" }) }
响应式UI(MVVM)
在Vue3中,ref
用于跟踪单个响应式值,而reactive
用于跟踪一个对象。
以下是使用ref
和reactive
的简单示例:
<template> <div> <!-- 使用ref的示例 --> <input v-model="messageRef" /> <p>{{ messageRef }}</p> <!-- 使用reactive的示例 --> <input v-model="state.name" /> <p>{{ state.name }}</p> </div> </template> <script> import { ref, reactive } from 'vue'; export default { setup() { // 使用ref const messageRef = ref('Hello, Vue!'); // 使用reactive const state = reactive({ name: 'Reactive State' }); // 返回到模板 return { messageRef, state }; } }; </script>
组件间通信
父子组件通信:
使用props 和 events
子父组件通信:
使用 emit和emit 和 emit和on
兄弟组件通信/父孙之间通信:
使用事件总线(Event Bus)或Vuex
跨多层级的组件通信:
使用事件总线或Vuex,或者provide/inject
示例:
<!-- 父组件 Parent.vue --> <template> <div> <Child :message="parentMessage" @notify="handleNotify" /> <p>来自子组件的消息: {{ childMessage }}</p> </div> </template> <script> import { ref } from 'vue'; import Child from './Child.vue'; export default { components: { Child, }, setup() { const parentMessage = ref('父组件的消息'); const childMessage = ref(''); const handleNotify = (message) => { childMessage.value = message; }; return { parentMessage, childMessage, handleNotify, }; }, }; </script> <!-- 子组件 Child.vue --> <template> <div> <p>{{ message }}</p> <button @click="sendMessage">向父组件发送消息</button> </div> </template> <script> import { defineComponent, ref } from 'vue'; export default defineComponent({ props: { message: String, }, setup(props, { emit }) { const sendMessage = () => { emit('notify', '子组件的消息'); }; return { sendMessage, }; }, }); </script>
响应式UI(MVVM)
Flutter 中UI可变化的组件是StatefulWidget ,更新组件状态并重新构建其 UI 的方法是setState
。当 StatefulWidget
的状态发生变化时,需要调用 setState
来通知 Flutter 框架,以便框架可以重新构建该组件的 UI。
以下是setState()的简单使用示例:
class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { // 这里更新UI相关的状态 _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( '点击按钮更新次数:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } }
Widget间通信
父子间通信:
子父间通信:
使用全局状态管理工具:
使用InheritedWidget:
示例:
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class ParentWidget extends StatefulWidget { @override _ParentWidgetState createState() => _ParentWidgetState(); } class _ParentWidgetState extends State<ParentWidget> { String messageFromChild = ''; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('父组件'), ), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('接受的子组件消息: $messageFromChild'), ChildWidget( initialMessage: '来自父亲的关怀', onMessageReceived: (msg) { setState(() { messageFromChild = msg; }); }, ), ], ), ); } } class ChildWidget extends StatefulWidget { final String initialMessage; final Function(String) onMessageReceived; ChildWidget({required this.initialMessage, required this.onMessageReceived}); @override _ChildWidgetState createState() => _ChildWidgetState(); } class _ChildWidgetState extends State<ChildWidget> { late String message; @override void initState() { super.initState(); message = widget.initialMessage; } @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('父亲的传递的关怀: $message'), ElevatedButton( onPressed: () { widget.onMessageReceived('来自儿子的孝心'); }, child: Text('喊爸爸'), ), ], ), ); } }
在ArKTS中,组件间通信主要有以下几种方式:
父子间通信:
通过构造函数传递数据:在创建子组件时,通过构造函数将数据传递给子组件。父组件中使用@State注解声明变量
子组件通过构造函数接受该变量.注解声明变量的方式有两种
@Prop
注解声明:父组件将变量「拷贝」一份交给子组件使用,子组件不可修改变量,@Link
注解声明;父组件会将变量的「引用」交给子组件,相当于子组件可以[直接修改]@ObjectLink
和@Observed
装饰器用于在涉及嵌套对象或数组为元素对象的场景中进行双向数据同步。子父间通信:
使用回调函数:在创建子Widget时,将回调函数操作传递给子Widget,子Widget执行操作后通过回调函数将结果传递回父Widget
父孙之间通信:
通过@provide
和@cousume
注解声明变量: 父组件中通过@provide
声明变量,发布消息订阅 子组件中通过@cousume
接收消息订阅.
注意:provide和cousume需要变量名字相同或者定义别名
示例:
@Entry @Component struct Index { @State message: string = 'Hello World' @State parentText: string = '我是 parent' @State childText: string = 'child-text' build() { Row() { Column() { Text(this.message) .fontSize(20) .fontWeight(FontWeight.Bold) .onClick((e) => { console.log(e + ""); this.message = "点击刷新message" }).margin({ bottom: 20 }); ChildComponent({ parentText: this.parentText, childText: $childText }).margin({ bottom: 20 }); Text("child text=" + this.childText).fontColor("#787").fontSize(30); } .width('100%') } .height('100%') } } @Component struct ChildComponent { @Prop parentText: string @Link childText: string build() { Text(this.parentText+"点击传递值给parent") .fontColor("#333") .fontSize(20) .onClick((e) => { this.childText = "child changed" }) } }
在声明式 UI 中,状态管理是通过将应用程序的状态从各个组件中进行状态提审到中央位置上。这样所有组件都可以共享和访问相同的状态数据。状态管理的目的是确保应用程序状态的变化变得可预测和可控,并保持不同部分的 UI 同步。
在前端开发中,登录成功后保存用户信息并且刷新全局UI是一个非常普遍的功能,我们通过用户信息全局状态管理进行讲解示例.
在Vue3,我们可以使用Vuex
或者是provide
和inject
实现全局状态管理
这里将使用provide
和inject
进行说明
创建全局状态管理模块
首先,我们创建一个名为 useUserStore.js
的文件,该文件将包含我们的全局用户状态及其相关方法。
<!-- useUserStore.js --> import { reactive, ref } from 'vue'; export const useUserStore = () => { const state = reactive({ user: ref(null) // 使用 ref 创建一个响应式引用,初始值为 null }); // 设置用户的方法 const setUser = (newUser) => { state.user = ref(newUser); // 当设置用户时,确保它是一个响应式引用 }; // 清除用户的方法 const clearUser = () => { state.user = ref(null); // 清除用户信息 }; // 提供给子组件的状态和方法 const provideUserStore = () => ({ user: state.user, setUser, clearUser }); return { provideUserStore }; };
在 Vue 应用中使用全局状态管理模块
接下来,在我们的主文件 main.js 中,我们需要使用创建的 useUserStore,并将其提供给根组件,这样任何子孙组件都可以注入它。
<!-- main.js --> import { createApp } from 'vue'; import App from './App.vue'; import { useUserStore } from './useUserStore'; const app = createApp(App); // 创建用户状态管理实例 const userStore = useUserStore(); // 将用户状态管理实例提供给根组件 app.provide('userStore', userStore.provideUserStore()); app.mount('#app');
在组件中使用全局状态管理
现在,在任何子孙组件中,我们都可以使用 inject
来访问用户状态和方法。
登录保存UserInfo
<!-- Login.vue --> <template> <div> <!-- 登录表单 --> <input type="text" v-model="credentials.username" placeholder="Username" /> <input type="password" v-model="credentials.password" placeholder="Password" /> <button @click="login">登录</button> </div> </template> <script> import { inject, ref } from 'vue'; export default { setup() { // 注入用户状态管理实例 const { user, setUser, clearUser } = inject('userStore'); // 用于存储登录凭据的响应式对象 const credentials = ref({ username: '', password: '' }); // 登录方法 const login = () => { // 登录成功,设置用户信息 setUser(credentials.value); }; // 清除用户信息的方法 const logout = () => { clearUser(); }; return { credentials, login, logout }; } }; </script>
User页面显示信息
<!-- User.vue --> <template> <div> <p v-if="user">{{ user.username }},欢迎回来!</p> <p v-else>请登录</p> </div> </template> <script> import { inject } from 'vue'; export default { setup() { // 注入用户状态管理实例 const { user } = inject('userStore'); return { user }; } }; </script>
在这个示例中,Login.vue 组件使用 inject 来获取全局状态管理实例,并调用 setUser 方法来设置用户信息。User.vue 组件也使用 inject 来获取用户信息,并显示当前登录用户的用户名。这样,无论用户导航到应用的哪个部分,用户状态都会保持一致,并且可以在需要时轻松更新。
在Flutter中,通常使用provider库实现全局状态管理.
下面是一个简单的示例,实现用户登录并保存用户信息以进行全局状态管理
首先,创建一个User类来表示用户信息:
//User.dart
class User {
final String name;
final String passwrod;
User(this.name, this.passwrod);
}
然后,创建一个UserInfoModel类,该类将包含用户登录的逻辑,并更新用户状态:
//UserInfoModel.dart import 'package:flutter/material.dart'; class UserInfoModel with ChangeNotifier { User? _currentUser; User? get currentUser => _currentUser; Future<void> login(String name, String passwrod) async { // 模拟异步登录过程 await Future.delayed(Duration(seconds: 1)); _currentUser = User(name, passwrod); notifyListeners(); } void logout() { _currentUser = null; notifyListeners(); } }
接下来,在main.dart中将UserInfoModel注册全局的Provider
///main.dart import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider.value(value: UserInfoModel()), ], child: MaterialApp( home: MyHomePage(), )); } }
现在,在任何组件中,我们都可以使用 Provider
提供的方法操作的UserInfoModel
对象。
Login.dart中登录成功后保存数据
//Login.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import './user_info_model.dart'; class Login extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Login'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ TextField( decoration: InputDecoration(labelText: 'Username'), ), SizedBox(height: 16.0), // 添加一些间距 TextField( obscureText: true, // 隐藏密码 decoration: InputDecoration(labelText: 'Password'), ), SizedBox(height: 24.0), // 添加一些间距 ElevatedButton( onPressed: () { // 获取Provider中的UserModel实例 UserInfoModel userModel = context.watch<UserInfoModel>(); // 假设我们从输入框中获取了用户名和密码 final String username = 'test_user'; final String password = 'test_password'; userModel.login(username,password) }, child: Text('Login'), ), ], ), ), ); } }
当点击登录按钮时,我们从Provider
中获取UserInfoModel
的实例,并使用模拟的用户名和密码来调用UserInfoModel
中login
方法实现用户信息的全局刷新
在User.dart中使用UserInfoModel获取用户信息
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'user_info_model.dart'; class User extends StatelessWidget { @override Widget build(BuildContext context) { UserInfoModel userModel = context.watch<UserInfoModel>(); return Scaffold( appBar: AppBar( title: Text('User Profile'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ // 显示用户名的Text组件 Text( userInfoModel.name, style: Theme.of(context).textTheme.headline5, ), // 退出登录的按钮 ElevatedButton( onPressed: () { userInfoModel.logout(); // 调用logout方法 }, child: Text( 'Logout', style: Theme.of(context).textTheme.button, ), ), ], ), ), ); } }
通过 context.watch<UserInfoModel>()
获取 UserInfoModel
的实例,并显示其 name
字段。ElevatedButton
组件的 onPressed
属性被设置为调用 UserInfoModel
实例的 logout
方法,处理退出登录,销毁用户数据的逻辑。
在鸿蒙ArkTS中,使用PersistentStorage 以及AppStorage进行全局状态管理
首先先说明一下PersistentStorage的作用为持久化保存AppStorage的对象,当退出App后,AppStorage保存的值将会被清除,PersistentStorage在初始化时,会将AppStorage的对象进行复原,已达到用户数据持久化保存
首先先建立UserInfoModel
//UserInfoModel.ets export class UserInfoModel { static init() { PersistentStorage.PersistProp('userInfo', { "isLogin": false }); } static login(loginInfo) { AppStorage.SetOrCreate('userInfo', loginInfo) } static logout() { PersistentStorage.DeleteProp('userInfo'); } }
然后进行初始化UserInfoModel,并且注册PersistentStorage
//Index.ets
//组件即将出现的生命周期方法
aboutToAppear() {
UserInfoModel.init()
}
就这样完成了UserInfoModel的全局注册
Login.ets中保存用户信息
import router from '@ohos.router' import toast from '@ohos.promptAction' @Entry @Component struct LoginPage { @State inputPhone: string = ""; build() { Row() { Column() { this._buildBackIcon() this._buildLoginTitle() this._buildPhoneTextFiled() this._buildSubmitButton() } .alignItems(HorizontalAlign.Start) .width('100%') } .alignItems(VerticalAlign.Top) .justifyContent(FlexAlign.Start) .height('100%').backgroundColor("#ffffff") } @Builder _buildBackIcon() { Row() { Image($r('app.media.ic_book_reader_left_arrow')) .width(14).height(14) } .width(40) .height(40) .margin({ top: 60, left: 20 }) .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) .onClick((e => { router.back() })) } @Builder _buildLoginTitle() { Text("手机号码登录/注册") .fontColor("#cc000000") .fontSize(26) .fontWeight(500) .margin({ top: 40, left: 30 }) } @Builder _buildPhoneTextFiled() { Column() { TextInput({ placeholder: "请输入手机号码" }) .height(50) .backgroundColor("#ffffff") .type(InputType.PhoneNumber) .onFocus(() => { }) .onChange((value: string) => { this.inputPhone = value }) .onSubmit((e) => { }) .maxLength(11) .placeholderColor("#80000000") .fontColor("#cc000000") .fontWeight(600) .margin(0) .padding(0) .borderRadius(0) Divider() .color(this.inputPhone.length == 0 ? "#2342" : "#234221") .height(0.5) .width('100%') } .padding({ left: 30, right: 30, top: 60 }) .width('100%') } @Builder _buildSubmitButton() { Row() { Button("立即登录") .width('100%') .height(44) .backgroundColor("#6236FF") .onClick((e) => { if (this.inputPhone.length == 0) { toast.showToast({ message: "请输入电话号码" }) return; } //模拟登录 const userInfo = UserApi.login(this.inputPhone) //刷新状态管理器 UserInfoModel.login(userInfo); }) }.margin({ left: 30, right: 30, top: 60 }) } }
User.ets中使用UserInfoModel的信息
//User.ets
@Entry @Component struct User { @StorageProp("userInfo") userInfo: string = "" build() { Row() { Column() { Text(JSON.stringify(this.userInfo)) .fontSize(20) .fontWeight(FontWeight.Bold) .onClick((e) => { console.log(e + ""); this.message = "sss" }).margin({ bottom: 20 }); Button("退出登录").onClick(() => { UserInfoModel.logout(); }) } .width('100%') } .height('100%') } }
在User.ets
中使用@StorageProp
注解获取到UserInfo的信息,("userInfo")
中的值需要和UserInfoModel
注册的值保持一样. 在退出登录时,调用 UserInfoModel.logout()
完成用户数据的清除
在日常开发中,我们所使用到的布局样式通常为Flex
(横向,纵向)和Position
(堆叠),这里主要也就从这2个方面来介绍三种语言的布局以及着重介绍AskTS的布局方式和常用的组件
在Vue中,实现布局需要使用HTML语言和CSS语言分别提供标签和样式。HTML语言提供标签,而CSS语言提供样式。这些代码分别放置在Vue组件的<template>
标签和<style>
标签之下。
Flex
<template> <div class="flex-container"> <div class="item">Item 1</div> <div class="item">Item 2</div> <div class="item">Item 3</div> </div> </template> <style> .flex-container{ display: flex; flex-direction:row; // row表示横向布局,column表示纵向布局 justify-content: center; //主轴上子元素的对齐方式 align-items: center; //交叉轴上子元素的对齐方式 } </style> 最终UI展示效果 item1 item2 item3横向或者是纵向排列
Position
<template> <div class="flex-container stack-container"> <div class="item">Item 1</div> <div class="item">Item 2</div> <div class="item">Item 3</div> <div class="item stack-item">Item 4</div> </div> </template> <style> .flex-container { display: flex; flex-direction: row; } .stack-container { position: relative; } .stack-item { position: absolute; top: 10px; //距离顶部距离 left: 10px; //距离左侧距离 //注意,同一方向的属性只能出现一个 //使用top left 布局从左上计算偏移 //使用top right 布局从右上计算偏移 //使用bottom left 布局从左下计算偏移 //使用bottom right 布局从右下计算偏移 background-color: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px; box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1); } </style> 最终UI展示效果 item1 item2 item3横向排列,item4盖在了item1上面
在Flutter中,万物皆为Widget,所有的布局和组件都由Widget组成。所有需要设置的属性都需要使用Widget,例如设置间距使用Pading组件,设置点击事件使用GestureDetector组件,设置背景颜色使用Container组件.所以在使用Flutter编写布局存在很严重的Widget嵌套过深的问题!
Flutter有两个基础Widget,所有的组件都是集成他俩.分别是StatelessWidget
和StatefulWidget
:
StatelessWidget(无状态组件):
StatefulWidget(有状态组件):
StatefulWidget代表着Widget的UI状态可以变化,即其属性可以在其生命周期内发生变化。
StatefulWidget通常用于表示需要动态交互的组件,例如按钮、输入框等。
StatefulWidget由两部分组成:一个是StatefulWidget本身,另一个是与之关联的State对象,用于管理Widget的状态。
StatefulWidget的build方法会在每次需要重建时被调用,并且可以通过与其关联的State对象来管理和更新状态。
Flex
RowExample.dart //Row 水平方向布局 class RowExample extends StatelessWidget { @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, //主轴,当组件是Row时,代表着设置组件水平方向的对齐方式 crossAxisAlignment: CrossAxisAlignment.center,//交叉轴,当组件是Row时,代表着设置组件垂直方向的对齐方式 children: [ Container( width: 50, height: 50, color: Colors.red, ), Container( width: 50, height: 50, color: Colors.green, ), Container( width: 50, height: 50, color: Colors.blue, ), ], ); } } //UI最终展示效果是red green blue三个色块横向排列 //Column 垂直方向布局 class ColumnExample extends StatelessWidget { @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, //主轴,当组件是Column时,代表着设置组件垂直方向的对齐方式 crossAxisAlignment: CrossAxisAlignment.start,//交叉轴,当组件是Column时,代表着设置组件水平方向的对齐方式 children: [ Container( width: 50, height: 50, color: Colors.red, ), Container( width: 50, height: 50, color: Colors.green, ), Container( width: 50, height: 50, color: Colors.blue, ), ], ); } } //UI最终展示效果是red green blue三个色块纵向排列
Position
在Flutter中实现堆叠布局使用的是Stack组件,在Stack组件中定位子Widget的位置有两种方式
//堆叠布局 class StackDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Stack( children: [ Container( width: double.infinity, height: double.infinity, color: Colors.white, ), //Stack中间 Align( alignment: Alignment.center, child: Container( width: 200, height: 200, color: Colors.blue, ), ), //Stack右上角 Align( alignment: Alignment.topRight, child: Container( width: 100, height: 100, color: Colors.yellow, ), ), //距离左侧50,顶部50的位置 Positioned( top: 50, left: 50, child: Container( width: 100, height: 100, color: Colors.red, ), ), //距离右侧50,底部50的位置 Positioned( bottom: 50, right: 50, child: Container( width: 100, height: 100, color: Colors.green, ), ), ], ); } } UI最终展示效果是blue在中间 yellow在右上角,red在距离左侧50,顶部50的位置,绿色在距离右侧50,底部50的位置
布局类型 | 优点 | 适用场景 |
---|---|---|
线性布局(Row、Column) | 简单,适合线性排列的子元素 | 子元素能够以某种方式线性排列时优先考虑此布局 |
层叠布局(Stack) | 能够实现堆叠效果而不影响其他子组件的布局空间 | 需要有堆叠效果时优先考虑此布局,例如弹出式面板 |
弹性布局(Flex) | 默认支持子组件的压缩或拉伸,可使得多个容器内子组件有更好的填充效果 | 需要计算拉伸或压缩比例时优先使用此布局 |
相对布局(RelativeContainer) | 布局自由,通过设置锚点规则实现子组件位置对齐 | 页面元素分布复杂、需要子元素自由布局时推荐使用 |
栅格布局(GridRow、GridCol) | 栅格可根据不同设备灵活布局,降低适配成本,保证一致性和协调性 | 不同设备、不同状态下的布局需求,例如手机、大屏、平板等 |
媒体查询(@ohos.mediaquery) | 根据设备类型或状态修改应用样式,提高用户体验 | 根据设备和应用的不同属性信息设计不同的布局,动态更新页面布局 |
列表(List) | 轻松高效地显示结构化、可滚动的信息 | 呈现同类数据类型或数据类型集的场景,例如图片和文本列表 |
网格(Grid) | 页面均分能力强,子组件占比控制能力强 | 需要按照固定比例或均匀分配空间的布局场景,例如计算器、相册、日历等 |
轮播(Swiper) | 实现广告轮播、图片预览等功能 | 广告轮播、图片预览、可滚动应用等场 |
布局类型 | 实现方式 | 使用说明 |
---|---|---|
绝对定位 | 使用 position 实现绝对定位,设置元素左上角相对于父容器左上角偏移位置 | 对于不同尺寸的设备,使用绝对定位的适应性会比较差,在屏幕的适配上有缺陷。 |
相对定位 | 使用 offset 可以实现相对定位,设置元素相对于自身的偏移量 | 相对定位不脱离文档流,即原位置依然保留,不影响元素本身的特性,仅相对于原位置进行偏移。 |
整体页面布局
页面分为三个模块,顶部功能区UgcDetailHeader
,底部功能区UgcDetailBottom
,中间内容显示区域this._buildUgcContent()
,顶部功能区和底部功能区盖在中间内容显示区域之上
build() { //完成底部模块UgcDetailBottom沉底 Stack({ alignContent: Alignment.BottomStart }) { //完成顶部模块UgcDetailHeade置顶 Stack({ alignContent: Alignment.TopStart }) { //Ugc内容模块 this._buildUgcContent() //顶部模块,包含title,左侧退出按钮,右侧分享按钮 UgcDetailHeader() }.width('100%').height("100%") //底部模块 UgcDetailBottom() } }
顶部模块UgcDetailHeader
//@Component注解表示这是一个组件 @Component export struct UgcDetailHeader { build() { Row() { //左侧关闭页面的按钮 Row() { Image($r("app.media.ic_book_reader_left_arrow")).width(7.5).height(14) } .height(36) .width(36) .margin({ left: 16 }) .alignItems(VerticalAlign.Center) .justifyContent(FlexAlign.Center) .borderRadius(18) .backgroundColor("#33000000") //右侧分享按钮 Row() { Image($r("app.media.ic_share_copy_link")).width(36).height(36) } .height(36) .width(36) .margin({ right: 16 }) .alignItems(VerticalAlign.Center) .justifyContent(FlexAlign.Center) .borderRadius(18) .backgroundColor("#33000000") } .width("100%") //主轴组件两端对齐 .justifyContent(FlexAlign.SpaceBetween) .height(60) } }
顶部模块UgcDetailBottom
@Component export struct UgcDetailBottom { @State inputReview: string = ""; build() { Row() { Row() { Image($r("app.media.ic_ugc_detail_review")).width(20).height(20).margin({ left: 18, right: 4 }) //输入框 TextInput({ placeholder: "说点什么..." }) .onChange((value: string) => { this.inputReview = value console.log("_buildPhoneTextFiled===onChange" + value) }) .onSubmit((e) => { console.log("_buildPhoneTextFiled===onSubmit" + e) }) .fontColor("#cc000000") .placeholderColor("#5c000000") .placeholderFont({ size: 12, }) .fontSize(12) .width(195) .height(30) .backgroundColor('#F1F1F1') .margin(0) .padding(0) } .width(237) .height(30) .backgroundColor('#F1F1F1') .borderRadius(16) .margin({ left: 16 }) Row() { Image($r("app.media.ic_ugc_detail_encourage")).width(20).height(20).margin({ left: 18, right: 4 }) Text("356次鼓励").fontSize(12).fontWeight(600).fontColor("#6236FF") }.margin({right:16}) } //交叉轴垂直居中 .alignItems(VerticalAlign.Center) //主轴两端对齐 .justifyContent(FlexAlign.SpaceBetween) .height(60) .backgroundColor("#ffffff") .width("100%") } }
中间内容显示区域UgcContent
//自定义组件方法,只能在当前类中使用
@Builder _buildUgcContent() {
//UGC内容模块,整体滑动
Scroll() {
//纵向布局
Column() {
//自定义组件方法,只能在当前类中使用
this._buildImageSwiper()
//用户模块,全局组件
UgcDetailUserInfo()
//UGC信息,全局组件
UgcDetailUgcInfo()
}
//交叉轴的排列方式
.alignItems(HorizontalAlign.Start)
//主轴的排列方式
.justifyContent(FlexAlign.Start)
}.width('100%')
}
UgcDetailUserInfo.ets
@Component
export struct UgcDetailUserInfo {
build() {
Row() {
Row() {
Image("https://wegymercdn.gymcity.com.cn/coach_archives_3.0/080ae09c812e4f1ef0f5290d23ced62f.png")
.height(36)
.width(36).borderRadius(10)
Column() {
Text("营养师彭彭").fontSize(13).fontColor("#CC000000")
Text("betterWE 明星营养师").fontSize(10).fontColor("#66000000").margin({ top: 3 })
}.margin({
left: 8
}).alignItems(HorizontalAlign.Start)
}
Row() {
Text("+ 关注").fontSize(13).fontWeight(600).fontColor("#6236FF")
.margin({
left: 3
})
}
.borderRadius(12)
.backgroundColor('#0D6236FF')
.width(64)
.height(24)
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.Center)
}
.justifyContent(FlexAlign.SpaceBetween)
.padding({
left: 16,
right: 16 }
)
.width("100%")
.margin({
top: 20
})
}
}
UgcDetailUgcInfo.ets
@Component
export struct UgcDetailUgcInfo {
build() {
Column() {
Text("营养师推荐 逆袭大女主!").fontSize(13).fontWeight(600).fontColor("#CC000000")
Text("1⃣早八党粗粮碳水【全麦杂粮包】\n✅0蔗糖0反式脂肪➕全麦占比no.1\n某姆健康版平价替代~\n\n声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/687915
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。