【动图+代码】自适应上下/左右布局|鸿蒙动效开发笔记 03 原创

awa_Liny
发布于 2024-10-15 08:48
浏览
0收藏

这篇笔记将介绍一种 HarmonyOS NEXT 开发里,标题+内容根据屏幕大小,自适应选择上下/左右排列的布局实现方式!

ヽ( ̄ω ̄( ̄ω ̄〃)ゝ 前置知识:

​GIF 案例,从 .animation() 到 Curves.springMotion|鸿蒙动效笔记 01​

和一点点鸿蒙开发基础(工程结构、ArkTS 语法)。

实现效果:

【动图+代码】自适应上下/左右布局|鸿蒙动效开发笔记 03-鸿蒙开发者社区

参考代码:

整个笔记的展示项目已经开源:

​《动效堆》Demo on GitHub​

​​​​《动效堆》Demo on Gitee​

阅读笔记时可以(临时)单独对照浏览这个:

// 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


布局结构

【动图+代码】自适应上下/左右布局|鸿蒙动效开发笔记 03-鸿蒙开发者社区

【动图+代码】自适应上下/左右布局|鸿蒙动效开发笔记 03-鸿蒙开发者社区

额外说明

关于区域布局

这个案例通过给根部的 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 ̄)~

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2024-10-15 08:48:09修改
1
收藏
回复
举报
回复
    相关推荐