概述:

本人新手入门鸿蒙开发,最近做房贷计算器小应用,在显示每一期数据列表的时候,用到了Grid,花费心思颇多,记录下核心点。
碰到一个麻烦就是鸿蒙不同版本少量API不兼容,如API19上的函数,不一定在API20上能用。网上找到的一些例子时常各种报错不能运行,这对新手是极不友好,拔高了入门门槛。

软硬件平台:

运行测试平台:鸿蒙6.0.0(Mate 70 Pro模拟器),即API20
开发平台Dev Echo:版本6.0.0.858

难点说明:

常用的Grid基本用法示例如下:

// 示例:创建一个2x2的网格布局
@Entry
@Component
struct GridExample {
  build() {
    Grid() {
      GridItem() {
        Text('Item 1')
          .fontSize(16)
          .textAlign(TextAlign.Center)
      }
      GridItem() {
        Text('Item 2')
          .fontSize(16)
          .textAlign(TextAlign.Center)
      }
      GridItem() {
        Text('Item 3')
          .fontSize(16)
          .textAlign(TextAlign.Center)
      }
      GridItem() {
        Text('Item 4')
          .fontSize(16)
          .textAlign(TextAlign.Center)
      }
    }
    .columnsTemplate('1fr 1fr') // 定义2列,每列宽度相等
    .width('100%')
    .height('100%')
  }
}

根据实际需要,在这个基础上,需要解决以下问题:
1、文本显示宽度自适应
2、超出屏幕宽度,水平滚动
3、标题栏显示和内容列对齐,即宽度等宽水平滚动同步
4、不够屏幕宽居中显示

源码展示:

界面代码,夹杂对显示数字格式处理,在关键的地方写了注释

import { MeasureText, MediaQuery, router, window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { JmpPageAppState } from '../global/JmpPageAppState';
import EqPrincipalInterest from '../housemortgage/EqPrincipalInterest';
import { EqResultNode } from '../housemortgage/EqResultNode';
import IntegerFormat from '../util/IntegerFormat';

@Entry
@Component
struct ShowList {
  /* 房贷每一期的信息,对象数组 */
  @State listResult:EqResultNode[] = [];
  /* 每一列宽度,总共有五列,默认最少20 */
  @State cloumnFontWidth:number[] = [20, 20, 20, 20, 20];
  /* 标题栏高度 */
  @State s32TitleHeight:number = 20;
  /* Grid宽度,后面会随着每一列宽度调整跟着改变 */
  @State gridWidth:string = '100%';
  /* 每一列显示的比例,后面会随着每一列宽度调整跟着改变 */
  @State columnsTemplateStr:string = '2fr 1fr 1fr 1fr 1fr';

  aboutToAppear() {
   /* 获取计算好的房贷期数数据 */
    this.listResult = JmpPageAppState.listResult;
  }


  showGridItemFormShowStr(item: EqResultNode, indexRow: number, indexColumn: number, inFontSize: number) {
    let strShow: string = '';
    let s32FontWidth:number = 0;
    let s32I:number = 0;
    let s32TotalWidth = 0;
    let columnsTemplateStr:string = '';

    /* 下面一堆操作表示0-4列显示处理,时间显示对齐和金额四舍五入加小数点 */
    if (0 == indexColumn) {
      strShow =
        (indexRow + 1).toString() + ')' + Math.floor(item.s32YearMonth / 100).toString().padStart(4, '0') + '-' +
        Math.floor(item.s32YearMonth % 100).toString().padStart(2, '0');
    } else if (1 == indexColumn) {
      strShow = IntegerFormat.ShowMoney(item.lInterest + item.lPrincipal);
    } else if (2 == indexColumn) {
      strShow = IntegerFormat.ShowMoney(item.lInterest);
    } else if (3 == indexColumn) {
      strShow = IntegerFormat.ShowMoney(item.lPrincipal);
    } else if (4 == indexColumn) {
      strShow = IntegerFormat.ShowMoney(item.lRemindAmount);
    } else {
      strShow = '';
    }

    /* 测量每一方格显示文本宽度,额外留16的边界宽 */
    s32FontWidth = MeasureText.measureText({ textContent: strShow, fontSize: inFontSize.toString() }) + 16;
    /* 如果当前文本列宽度大于当前保存的宽度,就要重新调整宽度,取最宽的地方 */
    if (indexColumn < this.cloumnFontWidth.length && s32FontWidth > this.cloumnFontWidth[indexColumn]) {
      this.cloumnFontWidth[indexColumn] = s32FontWidth;
      s32TotalWidth = 0;
      columnsTemplateStr = '';
      /* 总的宽度(this.gridWidth)需要重新统计并生效,总的列比例(this.columnsTemplateStr)也要重新统计并生效 */
      for (s32I = 0; s32I < this.cloumnFontWidth.length; s32I++) {
        s32TotalWidth += this.cloumnFontWidth[s32I];
        columnsTemplateStr += this.cloumnFontWidth[s32I].toString() + 'fr' + ' ';
      }
      this.gridWidth = s32TotalWidth.toString() + 'px';
      this.columnsTemplateStr = columnsTemplateStr;
    }

    return strShow;
  }

  @Builder
  showGridItem(item: EqResultNode, indexRow:number) {
    /* 一行中的每一列处理,总共5列,第一列字体大小16,其他列字体大小18,左对齐 */
    ForEach([0, 1, 2, 3, 4], (itemNumberColumn: number, index: number) => {
      GridItem() {
        Text(this.showGridItemFormShowStr(item, indexRow, itemNumberColumn, itemNumberColumn == 0 ? 16 : 18))
          .fontSize(itemNumberColumn == 0 ? 16 : 18).maxLines(1).textAlign(TextAlign.Start)
      }.border({
        width: 1,
        color: Color.Green,
        style: BorderStyle.Solid
      }).padding(0)
    }, (itemNumberColumn: number, index: number) => {
      return (itemNumberColumn * index).toString();
    })
  }

  calShowGridTitleShowTextHeight(strItem: string, s32FontSize:number, s32IndexColumn: number) {
    /* 计算标题栏宽和高 */
    let sizeOptions = MeasureText.measureTextSize({ textContent: strItem, fontSize: s32FontSize.toString() });
    let s32FontHeight = sizeOptions.height;
    let s32FontWidth = sizeOptions.width;
    let s32TotalWidth:number = 0;
    let s32I:number = 0;
    let columnsTemplateStr:string = '';

    if (s32FontHeight != undefined && (typeof s32FontHeight == 'number')) {
      s32FontHeight += 6;
      if (this.s32TitleHeight < s32FontHeight) {
        /* 标题栏每一列,高度取最大值 */
        this.s32TitleHeight = s32FontHeight;
      }
    }

    /* 如果当前标题栏文本列宽度大于当前保存的宽度,就要重新调整宽度,取最宽的地方 */
    if (s32IndexColumn < this.cloumnFontWidth.length && (typeof s32FontWidth == 'number') && s32FontWidth > this.cloumnFontWidth[s32IndexColumn]) {
      this.cloumnFontWidth[s32IndexColumn] = s32FontWidth;
      s32TotalWidth = 0;
      columnsTemplateStr = '';
      /* 总的宽度(this.gridWidth)需要重新统计并生效,总的列比例(this.columnsTemplateStr)也要重新统计并生效 */
      for (s32I = 0; s32I < this.cloumnFontWidth.length; s32I++) {
        s32TotalWidth += this.cloumnFontWidth[s32I];
        columnsTemplateStr += this.cloumnFontWidth[s32I].toString() + 'fr' + ' ';
      }
      this.gridWidth = s32TotalWidth.toString() + 'px';
      this.columnsTemplateStr = columnsTemplateStr;
    }

    return strItem;
  }

  @Builder
  showGridTitle(strItem: string, s32FontSize:number, s32IndexColumn: number) {
    /* 显示标题栏中的一列 */
    GridItem() {
      Text(this.calShowGridTitleShowTextHeight(strItem, s32FontSize, s32IndexColumn))
        .fontSize(s32FontSize)
    }.border({
      width: 1,
      color: Color.Grey,
      style: BorderStyle.Solid
    })
  }

  build() {
    Scroll() {
      Column() {
        /* 标题5列 */
        Grid() {
          this.showGridTitle('期次', 16, 0)
          this.showGridTitle('本息', 16, 1)
          this.showGridTitle('利息', 16, 2)
          this.showGridTitle('本金', 16, 3)
          this.showGridTitle('余额', 16, 4)
        }
        .border({
          width: 1,
          color: Color.Gray,
          style: BorderStyle.Solid
        })
        .width(this.gridWidth)
        .height(this.s32TitleHeight.toString() + 'px')
        .columnsTemplate(this.columnsTemplateStr)

        /* 正文内容,即具体房贷期数,30年就是360期,5列360行 */
        Grid() {
          ForEach(this.listResult, (item: EqResultNode, index: number) => {
            this.showGridItem(item, index)
          }, (item: EqResultNode, index: number) => {
            return item.s32YearMonth.toString();
          })
        }
        .height('auto')
        .width(this.gridWidth)
        .columnsTemplate(this.columnsTemplateStr)
        .scrollBar(BarState.On)
        .columnsGap(2)
        .margin({bottom:(this.s32TitleHeight + 5).toString() + 'px'}) /* 没有这一行底部显示不全 */
        .border({
          width: 1,
          color: Color.Gray,
          style: BorderStyle.Solid
        })
      }.height('100%').justifyContent(FlexAlign.Center)
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.On)
    .width('100%')
  }
}

功能代码,无需过度关注,仅让感兴趣的能运行通过

export class EqResultNode {
  s32YearMonth:number = 0;
  dlMonthRate:number = 0.0;
  lInterest:number = 0;
  lPrincipal:number = 0;
  lRemindAmount:number = 0;
}
import { hilog } from "@kit.PerformanceAnalysisKit";

export default class IntegerFormat {

  public static DoubleToLongRoundOff(dlIn:number) {
    return Math.round(dlIn);
  }
  public static ShowMoney(s32Money:number) {
    return Math.floor(s32Money / 100).toString() + '.' + Math.floor(s32Money % 100).toString().padStart(2, '0')
  }

  public static ArabicNumIntegerToChinese(s64ArabicNumInteger:number) {
    let s32I:number = 0;
    let s64Tmp:number = 0;
    let strOut:string = '';
    let s32TmpE:number = 0;
    let bRemind0:boolean = false;

    if (0 == s64ArabicNumInteger) {
      return '零';
    }
    s64Tmp = 1;
    for (s32I = 0; s32I < 20; s32I++) {
      s64Tmp = s64Tmp * 10;
      if (s64ArabicNumInteger < s64Tmp) {
        s64Tmp = s64Tmp / 10;
        break;
      }
    }

    if (s32I >= 20) {
      return '';
    }
    if (s32I > 8) {
      strOut = IntegerFormat.ArabicNumIntegerToChinese(Math.floor(s64ArabicNumInteger / 100000000)) + '亿';
      s64ArabicNumInteger = s64ArabicNumInteger % 100000000;
    }

    while (s32I >= 0) {
      s32TmpE = Math.floor(s64ArabicNumInteger / s64Tmp) % 10;
      if (0 == s32TmpE) {
        bRemind0 = true;
      } else {
        if (bRemind0) {
          strOut += IntegerFormat.ArabicSingleNumIntegerToChinese(0);
        }
        bRemind0 = false;
        strOut += IntegerFormat.ArabicSingleNumIntegerToChinese(s32TmpE);
      }

      if (0 != s32TmpE) {
        switch (s32I % 4) {
          case 0:
            break;
          case 1:
            strOut += '十';
            break;
          case 2:
            strOut += '百';
            break;
          case 3:
            strOut += '千';
            break;
        }
      }
      s64Tmp = s64Tmp / 10;
      if (s32I == 4) {
        strOut += '万';
      }
      s32I--;
    }

    return strOut;
  }

  public static AfterDecimalPointChineseShow(dlDecimal:number) {
    let s32AfterDecimalInt:number = Math.round(dlDecimal * 100) % 100;
    let s32TmpInt:number = 0;
    let strOut:string = '';

    if (0 == s32AfterDecimalInt) {
      return strOut;
    }

    s32TmpInt = Math.floor(s32AfterDecimalInt / 10);
    if (0 != s32TmpInt) {
      strOut += (IntegerFormat.ArabicSingleNumIntegerToChinese(s32TmpInt) + '角');
    }

    s32TmpInt = s32AfterDecimalInt % 10;
    if (0 != s32TmpInt) {
      strOut += (IntegerFormat.ArabicSingleNumIntegerToChinese(s32TmpInt) + '分');
    }

    return strOut;
  }

  private static ArabicSingleNumIntegerToChinese(s32SingleNum:number) {
    switch (s32SingleNum) {
      case 0:
        return '零';
      case 1:
        return '一';
      case 2:
        return '二';
      case 3:
        return '三';
      case 4:
        return '四';
      case 5:
        return '五';
      case 6:
        return '六';
      case 7:
        return '七';
      case 8:
        return '八';
      case 9:
        return '九';
      default :
        return '';
    }
    return '';
  }
}

运行效果:

因为最多只能5M,比较短,注意看即能上下滑动,也能左右滑动,标题栏同步左右滚动
在这里插入图片描述

核心点分析:

1、文本显示宽度自适应
每一个显示的方格GridItem,包括标题栏,计算文本的宽度,当该列宽度大于记录的列宽度,则保存下来。
然后重新计算总的列宽度即Grid宽度,重新计算列的比例。列的宽度我试了很多方法不能直接设置宽度值,只能设置每一列的宽度比例。

2、超出屏幕宽度,水平滚动
Grid默认支持垂直滚动,不支持水平滚动,所以水平滚动要单独处理。
把两个Grid,一个标题Grid一个内容Grid,用Column包裹起来,再在外面用水平Scroll包裹。简化如下

build() {
    Scroll() {
      Column() {
        Grid() {}
        Grid() {}
        }.height('100%').justifyContent(FlexAlign.Center)
	}
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.On)
    .width('100%')
 }
    

3、标题栏显示和内容列对齐,即宽度等宽水平滚动同步
从上面简化代码看到:
把两个Grid,一个标题Grid一个内容Grid,用Column包裹起来,再在外面用水平Scroll包裹。

4、不够屏幕宽居中显示
调试了很久,从简化代码看到:

Column() {
}.height('100%').justifyContent(FlexAlign.Center)

关键语句:justifyContent(FlexAlign.Center)

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐