【动图+代码】自适应上下/左右布局|鸿蒙动效开发笔记 03 原创
这篇笔记将介绍一种 HarmonyOS NEXT 开发里,标题+内容根据屏幕大小,自适应选择上下/左右排列的布局实现方式!
ヽ( ̄ω ̄( ̄ω ̄〃)ゝ 前置知识:
GIF 案例,从 .animation() 到 Curves.springMotion|鸿蒙动效笔记 01
和一点点鸿蒙开发基础(工程结构、ArkTS 语法)。
实现效果:
参考代码:
整个笔记的展示项目已经开源:
阅读笔记时可以(临时)单独对照浏览这个:
// components/ResponsiveLayout.ets
// 封装好的自适应左右布局组件!
import Curves from '@ohos.curves';
import { fontSize_Extra_Large, fontSize_Large } from '../defaults/defaults';
@Entry
@Component
struct responsiveLayout {
// Basic info
@State title: ResourceStr = ""
@State subTitle: ResourceStr = ""
@State alignRules_left: AlignRuleOption = {
center: { anchor: "__container__", align: VerticalAlign.Center },
left: { anchor: "__container__", align: HorizontalAlign.Start }
}
@State alignRules_right: AlignRuleOption = {
center: { anchor: "__container__", align: VerticalAlign.Center },
right: { anchor: "__container__", align: HorizontalAlign.End }
}
@State alignRules_top: AlignRuleOption = {
top: { anchor: "__container__", align: VerticalAlign.Top },
middle: { anchor: "__container__", align: HorizontalAlign.Center }
}
@State alignRules_bottom: AlignRuleOption = {
bottom: { anchor: "__container__", align: VerticalAlign.Bottom },
middle: { anchor: "__container__", align: HorizontalAlign.Center }
}
@State alignLeftProportion: number = 0.3;
@State alignTopProportion: number = 0.5;
@State animationParam: AnimateParam = { curve: Curves.springMotion(0.3, 1) };
// Statuses
@State landscapeLayout: boolean = false;
@State areaWidth: number = 0;
@State areaHeight: number = 0;
@State content_height: number = 0;
// Pass in content components
@BuilderParam content_section: () => void;
build() {
Scroll() {
RelativeContainer() {
Column() {
Text(this.title)
.fontWeight(FontWeight.Bold)
.fontSize(fontSize_Extra_Large())
Text(this.subTitle)
.fontWeight(FontWeight.Bold)
.fontSize(fontSize_Large())
.opacity(0.5)
} // title section
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Start)
.padding(10)
.borderRadius(16)
// Position settings
.alignRules(this.landscapeLayout ? this.alignRules_left : this.alignRules_top)
.width((this.landscapeLayout ? this.alignLeftProportion : 1) * this.areaWidth)
.height((this.landscapeLayout ? 1 : this.alignTopProportion) * this.areaHeight)
.animation(this.animationParam)
Scroll() {
Column({ space: 10 }) {
this.content_section();
} // content section
.width("100%")
.onAreaChange((_o, n) => {
// Update content height
this.content_height = n.height as number + 30;
})
.animation(this.animationParam)
} // title section
.edgeEffect(EdgeEffect.Spring)
.padding(10)
.borderRadius(16)
.scrollBar(this.landscapeLayout ? BarState.Auto : BarState.Off)
.enableScrollInteraction(this.landscapeLayout ? true : false)
// Position settings
.alignRules(this.landscapeLayout ? this.alignRules_right : this.alignRules_bottom)
.width((this.landscapeLayout ? 1 - this.alignLeftProportion : 1) * this.areaWidth)
.height((this.landscapeLayout ? this.areaHeight : this.content_height))
.animation(this.animationParam)
} // base container of sub sections
.width("100%")
.height(this.landscapeLayout ? "100%" : (this.alignTopProportion * this.areaHeight + this.content_height))
.animation(this.animationParam)
} // The base of everything
.edgeEffect(EdgeEffect.Spring)
.padding({ left: this.landscapeLayout ? 30 : 10, right: 10, top: 10 })
.scrollBar(this.landscapeLayout ? BarState.Off : BarState.Auto)
.enableScrollInteraction(this.landscapeLayout ? false : true)
.onAreaChange((_o, n) => {
// Set display size
this.areaWidth = n.width as number - 20;
this.areaHeight = n.height as number - 20;
// determine tabletMode
if (this.areaWidth > 400) {
this.landscapeLayout = true;
} else {
this.landscapeLayout = false;
}
})
.width("100%")
.height("100%")
.animation(this.animationParam)
}
proportion_float_to_string(p: number) {
return (p * 100).toString() + "%";
}
}
export default
// defaults/defaults.ets
// 供整个项目统一调用的、类似常量的默认数据
export function fontSize_Extra_Large() {
return 36;
}
export function fontSize_Large() {
return 24;
}
export function fontSize_Normal() {
return 16;
}
export function fontSize_Icon_Button() {
return 25;
}
export function click_effect_default() {
let ce: ClickEffect = { level: ClickEffectLevel.LIGHT };
return
// pages/responsiveLayoutExample.ets
// 演示页面
import responsiveLayout from '../components/ResponsiveLayout';
import any_item from './FadeInExample';
@Entry
@Component
struct responsiveLayoutExample {
@State labels: string[] = ["Meow", "¯\\_(ツ)_/¯", "( •̀ ω •́ )✧", "在一起 Together", "(~o ̄3 ̄)~", "就可以!TECH4ALL"]
build() {
Column() {
responsiveLayout({
title: $r('app.string.index_example_responsiveLayout'),
subTitle: 'ヽ( ̄ω ̄( ̄ω ̄〃)ゝ',
}) {
ForEach(this.labels, (text: string, index: number) => {
any_item({ text: text, timeout: index })
})
}
}
.height("100%")
.width("100%")
}
}
整体思路:
使用的术语
横排/竖排:分别对应左右/上下布局;
标题区域:左/上区域,用于显示标题文字;
内容区域:右/下区域,用于显示自定义内容/组件。
竖排
首先,在竖排情况下,整个页面要能够滚动,因此选择 Scroll
然后,要实现两个区域的上下/左右切换,Liny 选择将万能的 RelativeContainer(因为布局很自由,并且切换布局位置(属性变化)时方便加动画)放在 Scroll 里。
接着,两个区域本身可以用任何容器实现,不过 Liny 希望横排的时候内容区域能够单独滚动起来,于是使用了 Scroll 套 Column
同时,在竖排情况下,我们希望上下两个区域是同级的,滚动的时候一起滚动,于是就要判断在竖排是锁定内容区域,让它和标题区域一起滚起来。
布局 alignRules 配置:
@State alignRules_top: AlignRuleOption = {
top: { anchor: "__container__", align: VerticalAlign.Top },
middle: { anchor: "__container__", align: HorizontalAlign.Center }
}
@State alignRules_bottom: AlignRuleOption = {
bottom: { anchor: "__container__", align: VerticalAlign.Bottom },
middle: { anchor: "__container__", align: HorizontalAlign.Center
横排
横排情况相对简单:我们希望滚动的时候标题不动,只滚动内容区域即可 → 于是锁定根容器 Scroll,解锁内容区域 Scroll。
布局 alignRules 配置:
@State alignRules_left: AlignRuleOption = {
center: { anchor: "__container__", align: VerticalAlign.Center },
left: { anchor: "__container__", align: HorizontalAlign.Start }
}
@State alignRules_right: AlignRuleOption = {
center: { anchor: "__container__", align: VerticalAlign.Center },
right: { anchor: "__container__", align: HorizontalAlign.End
布局结构
额外说明
关于区域布局
这个案例通过给根部的 Scroll 设置 onAreaChange 检测整个布局的大小,并且将得到的长宽减去间隔(10 vp)后保存。然后根据这组数据判断是否是横排模式(this.landscapeLayout)。
这样,两个区域的布局就可以根据这些数据,决定自己的布局方式:
// 标题区域:横排时排列到左边,竖排时排列到上边
.alignRules(this.landscapeLayout ? this.alignRules_left : this.alignRules_top)
// 内容区域:横排时排列到右边,竖排时排列到下边
.alignRules(this.landscapeLayout ? this.alignRules_right : this.alignRules_bottom)
也可以按比例计算后得到自己的尺寸了:
// 标题区域 Column 的属性:
// 横排时宽按比例乘积,高为整个显示区域的高;
// 竖排时宽为整个显示区域的宽,高为按比例乘积。
.width((this.landscapeLayout ? this.alignLeftProportion : 1) * this.areaWidth)
.height((this.landscapeLayout ? 1 : this.alignTopProportion) * this.areaHeight)
// 内容区域 Scroll 的属性
// 内容区域:宽度同理,不过高度在竖排的时候会被设置成内容子组件的高度,这样就能显示全所有的东西。
// 然后锁住 Scroll 的时候就不会挡住内容了。
.width((this.landscapeLayout ? 1 - this.alignLeftProportion : 1) * this.areaWidth)
.height((this.landscapeLayout ? this.areaHeight : this.content_height))
关于滚动
竖排时,为了实现两个布局的一体化滚动,这里禁用其滚动属性(.enableScrollInteraction()),让滚动操作由根部的大 Scroll 响应,就能滚在一起了。
.enableScrollInteraction(this.landscapeLayout ? true : false)
并且,为了在锁定后不遮挡内容区域内容,我们让内容区域的 Scroll 和其子组件 Column 高度相等。这需要我们检测 Column 高度的变化,并且同步到 Scroll。
.onAreaChange((_o, n) => {
// Update content height
this.content_height = n.height as number + 30;
})
(方案可以优化,期待评论区解法!)
横排时则解锁内容区域的滚动,锁住根部大 Scroll 的滚动,就能只滚内容区域了!
关于封装
Liny 在这里把这个东西封装了出来,只需要传入标题和副标题作为参数,以及一个或多个子组件即可使用。于是你会发现这个布局也出现在了《动画堆》Demo 的首页。
给自定义组件传入一个子组件参考:@BuilderParam 装饰器。
一些后话
同样的,这个文章所介绍的方案也存在诸多不足:代码冗长,属性众多……必然存在不小的优化空间。如果文章内容中含有错误,也请评论区各路大佬斧正!ORZ。
最后的最后,感谢你读到这里!咱们下一篇鸿蒙动效开发笔记再见!!(~o ̄3 ̄)~