HarmonyOS开发实战:如何实现一个运动排名榜页面

news/2024/7/21 11:53:08 标签: harmonyos, 华为

HarmonyOS开发实战:如何实现一个运动排名榜页面

代码仓库: 运动排名榜页面

项目介绍

本项目使用声明式语法和组件化基础知识,搭建一个可刷新的排行榜页面。在排行榜页面中,使用循环渲染控制语法来实现列表数据渲染,使用@Builder创建排行列表布局内容,使用装饰器@State、@Prop、@Link来管理组件状态。最后我们点击系统返回按键,来学习自定义组件生命周期函数。完成效果如图所示:

在这里插入图片描述

新建项目工程

选择Create Project新建项目,点击Application选择第一个Empty Ability应用,点击“Next”进行下一步
在这里插入图片描述
配置页中,详细信息如下:

Project name是开发者可以自行设置的项目名称,这里根据自己选择修改为自己项目名称。
Bundle name是包名称,默认情况下应用ID也会使用该名称,应用发布时对应的ID需要保持一致。
Save location为工程保存路径,建议用户自行设置相应位置。
Compile SDK是编译的API版本,这里默认选择API9。
Model选择Stage模型,其他保持默认即可。
点击“Finish”进行下一步
在这里插入图片描述

项目创建成功
在这里插入图片描述

代码实现

编写应用入口页面

RankPage是应用入口页面

@Entry
@Component
struct RankPage {
  @State message: string = 'My Ranking'

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}

修改EntryAbility.ts应用配置,入口页面更改为pages/RankPage
在这里插入图片描述
运行成功
在这里插入图片描述

编写公共常量

公共常量的好处包括:

  • 代码可读性: 使用公共常量可以提高代码的可读性和可维护性,因为它们可以清晰地表达出程序中使用的固定数值或字符串。
  • 便于修改和维护: 如果程序中的某个数值或字符串需要修改,只需修改公共常量的定义,而不需要在整个程序中逐个修改。
  • 避免魔法数值: 使用公共常量可以避免在代码中出现“魔法数值”,即难以理解和维护的硬编码数值。
  • 提高代码重用性: 公共常量可以在整个程序中被引用和使用,从而提高代码的重用性。
  • 统一管理: 将所有的固定数值或字符串集中管理在公共常量中,有利于统一管理和维护。
/**
 * The font size of application.
 */
export enum FontSize {
  SMALL = 14,
  MIDDLE = 16,
  LARGE = 20,
};

/**
 * The font weight of application.
 */
export enum FontWeight {
  BOLD = '400',
  BOLDER = '500',
};

/**
 * The weight is global default value for component size.
 */
export const WEIGHT = '100%';

/**
 * The duration of toast.
 */
export const TIME = 1000;

/**
 * The interval time of exit.
 */
export const APP_EXIT_INTERVAL: number = 4500;

/**
 * The tag is the page name,which is used to print.
 */
export const TAG: string = 'RankPage';

/**
 * The title of TitleComponent.
 */
export const TITLE: Resource = $r('app.string.title');

class style {
  RANK_PADDING: number = 15;
  CONTENT_WIDTH: string = '90%';
  BORDER_RADIUS: number = 20;
  STROKE_WIDTH: number = 1;
  HEADER_MARGIN_TOP: number = 20;
  HEADER_MARGIN_BOTTOM: number = 15;
  LIST_HEIGHT: string = '65%';
}

/**
 * The Style of RankPage.
 */
export const Style: style = {
  /**
   * The padding of ranking.
   */
  RANK_PADDING: 15,

  /**
   * The width of ranking content.
   */
  CONTENT_WIDTH: '90%',

  /**
   * The border radius.
   */
  BORDER_RADIUS: 20,

  /**
   * The stroke width of divider.
   */
  STROKE_WIDTH: 1,

  /**
   * The top margin of ranking header.
   */
  HEADER_MARGIN_TOP: 20,

  /**
   * The bottom margin of ranking header.
   */
  HEADER_MARGIN_BOTTOM: 15,

  /**
   * The height of list.
   */
  LIST_HEIGHT: '65%'
};

class listHeaderStyle {
  FONT_WEIGHT: number = 400;
  LAYOUT_WEIGHT_LEFT: string = '30%';
  LAYOUT_WEIGHT_CENTER: string = '50%';
  LAYOUT_WEIGHT_RIGHT: string = '20%';
}

/**
 * The Style of ListHeaderComponent.
 */
export const ListHeaderStyle: listHeaderStyle = {
  /**
   * The weight of font.
   */
  FONT_WEIGHT: 400,

  /**
   * The layout weight of left.
   */
  LAYOUT_WEIGHT_LEFT: '30%',

  /**
   * The layout weight of center.
   */
  LAYOUT_WEIGHT_CENTER: '50%',

  /**
   * The layout weight of right.
   */
  LAYOUT_WEIGHT_RIGHT: '20%',
};

class itemStyle {
  TEXT_LAYOUT_SIZE: number = 24;
  CIRCLE_TEXT_BORDER_RADIUS: number = 24;
  CIRCLE_TEXT_SIZE: number = 24;
  CIRCLE_TEXT_COLOR_STOP_1: number = 0.5;
  CIRCLE_TEXT_COLOR_STOP_2: number = 1.0;
  BAR_HEIGHT: number = 48;
  LAYOUT_WEIGHT_LEFT: string = '30%';
  LAYOUT_WEIGHT_CENTER: string = '50%';
  LAYOUT_WEIGHT_RIGHT: string = '20%';
  BORDER_WIDTH: number = 1;
  COLOR_GREEN: Resource = $r('app.color.item_color');
  COLOR_BLACK: Resource = $r('app.color.item_color_black');
}

/**
 * The Style of ListItemComponent.
 */
export const ItemStyle: itemStyle = {
  /**
   * The line height of text.
   */
  TEXT_LAYOUT_SIZE: 24,

  /**
   * The border radius of circle text.
   */
  CIRCLE_TEXT_BORDER_RADIUS: 24,

  /**
   * The size of circle text.
   */
  CIRCLE_TEXT_SIZE: 24,

  /**
   * Gradient color proportion.
   */
  CIRCLE_TEXT_COLOR_STOP_1: 0.5,

  /**
   * Gradient color proportion.
   */
  CIRCLE_TEXT_COLOR_STOP_2: 1.0,

  /**
   * The height of item.
   */
  BAR_HEIGHT: 48,

  /**
   * The layout weight of left.
   */
  LAYOUT_WEIGHT_LEFT: '30%',

  /**
   * The layout weight of center.
   */
  LAYOUT_WEIGHT_CENTER: '50%',

  /**
   * The layout weight of right.
   */
  LAYOUT_WEIGHT_RIGHT: '20%',

  /**
   * The width of border.
   */
  BORDER_WIDTH: 1,

  /**
   * The green color of item.
   */
  COLOR_GREEN: $r('app.color.item_color'),

  /**
   * The black color of item.
   */
  COLOR_BLACK: $r('app.color.item_color_black')
};

class titleBarStyle {
  IMAGE_BACK_SIZE: number = 21;
  IMAGE_BACK_MARGIN_RIGHT: number = 18;
  IMAGE_LOADING_SIZE: number = 22;
  BAR_HEIGHT: number = 47;
  BAR_MARGIN_HORIZONTAL: number = 26;
  BAR_MARGIN_TOP: number = 10;
  WEIGHT: string = '50%';
}

/**
 * The Style of TitleComponent.
 */
export const TitleBarStyle: titleBarStyle = {
  /**
   * The image size of back button.
   */
  IMAGE_BACK_SIZE: 21,

  /**
   * The right margin of back button.
   */
  IMAGE_BACK_MARGIN_RIGHT: 18,

  /**
   * The size of loading image.
   */
  IMAGE_LOADING_SIZE: 22,

  /**
   * The height of TitleComponent.
   */
  BAR_HEIGHT: 47,

  /**
   * The horizontal margin of TitleComponent.
   */
  BAR_MARGIN_HORIZONTAL: 26,

  /**
   * The top margin of TitleComponent.
   */
  BAR_MARGIN_TOP: 10,

  /**
   * The weight of Row layout.
   */
  WEIGHT: '50%',
};

静态资源

静态资源从代码仓库去获取
在这里插入图片描述

实现标题组件

TitleComponent.ets代码实现

import AppContext from '@ohos.app.ability.common';
import { FontSize, TitleBarStyle, WEIGHT } from '../common/constants/Constants';

// 标题组件
@Component
export struct TitleComponent {
  @Link isRefreshData: boolean; // 是否刷新数据
  @State title: Resource = $r('app.string.title_default'); // 标题

  build() {
    Row() {
      Row() {
        // 返回箭头
        Image($r('app.media.ic_public_back'))
          .height(TitleBarStyle.IMAGE_BACK_SIZE) // 高度
          .width(TitleBarStyle.IMAGE_BACK_SIZE) // 宽度
          .margin({ right: TitleBarStyle.IMAGE_BACK_MARGIN_RIGHT }) // 外边距
          .onClick(() => { // 返回上一个界面
            let handler = getContext(this) as AppContext.UIAbilityContext;
            handler.terminateSelf();
          })
        Text(this.title) // 标题
          .fontSize(FontSize.LARGE)
      }
      .width(TitleBarStyle.WEIGHT)
      .height(WEIGHT)
      .justifyContent(FlexAlign.Start) // 水平起始位置对齐

      // 刷新图标
      Row() {
        Image($r('app.media.loading'))
          .height(TitleBarStyle.IMAGE_LOADING_SIZE)
          .width(TitleBarStyle.IMAGE_LOADING_SIZE)
          .onClick(() => { // 点击刷新列表数据
            this.isRefreshData = !this.isRefreshData;
          })
      }
      .width(TitleBarStyle.WEIGHT)
      .height(WEIGHT)
      .justifyContent(FlexAlign.End)
    }
    .width(WEIGHT)
    .padding({ left: TitleBarStyle.BAR_MARGIN_HORIZONTAL,
      right: TitleBarStyle.BAR_MARGIN_HORIZONTAL })
    .margin({ top: TitleBarStyle.BAR_MARGIN_TOP })
    .height(TitleBarStyle.BAR_HEIGHT)
    .justifyContent(FlexAlign.SpaceAround)
  }
}

应用入口页RankPage引入TitleComponent组件

import { APP_EXIT_INTERVAL, Style, TIME, TITLE, WEIGHT } from '../common/constants/Constants';
import { TitleComponent } from '../view/TitleComponent';

@Entry
@Component
struct RankPage {

  @State isSwitchDataSource: boolean = true; // 是否切换数据源

  build() {
    Column() {
      // 标题
      TitleComponent({ isRefreshData: $isSwitchDataSource, title: TITLE })
    }
  }
}

运行代码:
在这里插入图片描述

实现列表头部

ListHeaderComponent .ets代码实现

import { FontSize, ListHeaderStyle } from '../common/constants/Constants';

@Component
export struct ListHeaderComponent {
  paddingValue: Padding | Length = 0; // 内边距
  widthValue: Length = 0; // 宽度

  build() {
    Row() {
      Text('排名')
        .fontSize(FontSize.SMALL) // 字体大小
        .width(ListHeaderStyle.LAYOUT_WEIGHT_LEFT) // 宽度
        .fontWeight(ListHeaderStyle.FONT_WEIGHT) // 字体宽度
        .fontColor($r('app.color.font_description')) // 字体颜色
      Text('姓名')
        .fontSize(FontSize.SMALL)
        .width(ListHeaderStyle.LAYOUT_WEIGHT_CENTER)
        .fontWeight(ListHeaderStyle.FONT_WEIGHT)
        .fontColor($r('app.color.font_description'))
      Text('步数')
        .fontSize(FontSize.SMALL)
        .width(ListHeaderStyle.LAYOUT_WEIGHT_RIGHT)
        .fontWeight(ListHeaderStyle.FONT_WEIGHT)
        .fontColor($r('app.color.font_description'))
    }
    .width(this.widthValue) // 宽度
    .padding(this.paddingValue) // 内边距
  }
}

应用入口页RankPage引入ListHeaderComponent 组件

import { APP_EXIT_INTERVAL, Style, TIME, TITLE, WEIGHT } from '../common/constants/Constants';
import { TitleComponent } from '../view/TitleComponent';
import { ListHeaderComponent } from '../view/ListHeaderComponent';

@Entry
@Component
struct RankPage {

  @State isSwitchDataSource: boolean = true; // 是否切换数据源

  build() {
    Column() {
      // 标题
      TitleComponent({ isRefreshData: $isSwitchDataSource, title: TITLE })
      // 列表头部
      ListHeaderComponent({
        paddingValue: {
          left: Style.RANK_PADDING,
          right: Style.RANK_PADDING
        },
        widthValue: Style.CONTENT_WIDTH
      })
        .margin({ // 外边距
          top: Style.HEADER_MARGIN_TOP,
          bottom: Style.HEADER_MARGIN_BOTTOM
        })
    }
  }
}

运行代码:
在这里插入图片描述

准备排名榜数据

RankData.ets 排序类

// 排名类
export class RankData {
  name: string; // 姓名
  stepNum: string; // 步数
  id: string;

  // 构造函数
  constructor(id: string, name: string, stepNum: string) {
    this.id = id;
    this.name = name;
    this.stepNum = stepNum;
  }
}

DataModel.ets初始化排名数据

import { RankData } from '../viewmodel/RankData';

export { rankData1, rankData2 }

// 初始化排名数据1
const rankData1: RankData[] = [
  new RankData('1', '喜羊羊', '12080'),
  new RankData('2', '美羊羊', '10320'),
  new RankData('3', '灰太狼', '9801'),
  new RankData('4', '红太狼', '8431'),
  new RankData('5', '懒羊羊', '7546'),
  new RankData('6', '暖羊羊', '7431'),
  new RankData('7', '沸羊羊', '7187'),
  new RankData('8', '蕉太狼', '7003'),
  new RankData('9', '小灰灰', '6794'),
  new RankData('10', '慢羊羊', '6721')
];

// 初始化排名数据2
const rankData2: RankData[] = [
  new RankData('11', '曹操', '8836'),
  new RankData('12', '马超', '8521'),
  new RankData('13', '关羽', '8431'),
  new RankData('14', '吕布', '7909'),
  new RankData('15', '张飞', '7547'),
  new RankData('16', '赵云', '7433'),
  new RankData('17', '刘备', '7186'),
  new RankData('18', '孙策', '7023'),
  new RankData('19', '黄忠', '6794'),
  new RankData('20', '许褚', '6721')
];

RankViewModel.ets获取排序数据

import { RankData } from './RankData';
import { rankData1, rankData2 } from '../model/DataModel';

// 获取排序数据
export class RankViewModel {
  loadRankDataSource1(): RankData[] {
    return rankData1;
  }

  loadRankDataSource2(): RankData[] {
    return rankData2;
  }
}

RankPage.ets引入排名数据源

  • let rankModel: RankViewModel = new RankViewModel();
  • 通过aboutToAppear函数初始化数据源
import { RankViewModel } from '../viewmodel/RankViewModel';
import { RankData } from '../viewmodel/RankData';
import { APP_EXIT_INTERVAL, Style, TIME, TITLE, WEIGHT } from '../common/constants/Constants';
import { TitleComponent } from '../view/TitleComponent';
import { ListHeaderComponent } from '../view/ListHeaderComponent';

let rankModel: RankViewModel = new RankViewModel();

@Entry
@Component
struct RankPage {

  @State dataSource1: RankData[] = [];
  @State dataSource2: RankData[] = [];
  @State isSwitchDataSource: boolean = true; // 是否切换数据源

  // 初始化数据源
  aboutToAppear() {
    this.dataSource1 = rankModel.loadRankDataSource1();
    this.dataSource2 = rankModel.loadRankDataSource2();
  }

  build() {
    Column() {
      // 标题
      TitleComponent({ isRefreshData: $isSwitchDataSource, title: TITLE })
      // 列表头部
      ListHeaderComponent({
        paddingValue: {
          left: Style.RANK_PADDING,
          right: Style.RANK_PADDING
        },
        widthValue: Style.CONTENT_WIDTH
      })
        .margin({ // 外边距
          top: Style.HEADER_MARGIN_TOP,
          bottom: Style.HEADER_MARGIN_BOTTOM
        })
    }
  }
}

实现排名列表

ListItemComponent .ets代码实现

import { FontSize, FontWeight, ItemStyle, WEIGHT } from '../common/constants/Constants';

@Component
export struct ListItemComponent {
  index?: number;
  private name?: string;
  stepNum: string = '';
  @State isChange: boolean = false;

  build() {
    Row() {
      // 排名
      Column() {
          if (this.isRenderCircleText()) {
            this.CircleText(this.index);
          } else {
            Text(this.index?.toString())
              .lineHeight(ItemStyle.TEXT_LAYOUT_SIZE) // 行高
              .textAlign(TextAlign.Center) // 文本居中
              .width(ItemStyle.TEXT_LAYOUT_SIZE) // 宽度
              .fontWeight(FontWeight.BOLD) // 字体宽度
              .fontSize(FontSize.SMALL) // 字体大小
          }
      }
      .width(ItemStyle.LAYOUT_WEIGHT_LEFT) // 宽度
      .alignItems(HorizontalAlign.Start) // 垂直起始位置对齐

      // 姓名
      Text(this.name)
        .width(ItemStyle.LAYOUT_WEIGHT_CENTER)
        .fontWeight(FontWeight.BOLDER)
        .fontSize(FontSize.MIDDLE)
        // ture:绿色字体,false:黑色字体
        .fontColor(this.isChange ? ItemStyle.COLOR_GREEN : ItemStyle.COLOR_BLACK)
      // 步数
      Text(this.stepNum)
        .width(ItemStyle.LAYOUT_WEIGHT_RIGHT)
        .fontWeight(FontWeight.BOLD)
        .fontSize(FontSize.SMALL)
        .fontColor(this.isChange ? ItemStyle.COLOR_GREEN : ItemStyle.COLOR_BLACK)
    }
    .height(ItemStyle.BAR_HEIGHT)
    .width(WEIGHT)
    .onClick(() => { // 点击事件
      this.isChange = !this.isChange;
    })
  }

  // 圆圈背景
  @Builder CircleText(index: number) {
    Row() {
      Text(this.index?.toString())
        .fontWeight(FontWeight.BOLD)
        .fontSize(FontSize.SMALL)
        .fontColor(Color.White);
    }
    .justifyContent(FlexAlign.Center)
    .borderRadius(ItemStyle.CIRCLE_TEXT_BORDER_RADIUS)
    .size({ width: ItemStyle.CIRCLE_TEXT_SIZE,
      height: ItemStyle.CIRCLE_TEXT_SIZE })
    .backgroundColor($r('app.color.circle_text_background'))
  }

  // 是否显示圆圈
  isRenderCircleText(): boolean {
    return this.index === 1 || this.index === 2 || this.index === 3;
  }
}

应用入口页RankPage引入ListItemComponent组件

  • @Builder RankList装饰的方法用于定义组件的声明式UI描述,在一个自定义组件内快速生成多个布局内容。
import { RankViewModel } from '../viewmodel/RankViewModel';
import { RankData } from '../viewmodel/RankData';
import { APP_EXIT_INTERVAL, Style, TIME, TITLE, WEIGHT } from '../common/constants/Constants';
import { TitleComponent } from '../view/TitleComponent';
import { ListHeaderComponent } from '../view/ListHeaderComponent';
import { ListItemComponent } from '../view/ListItemComponent';

let rankModel: RankViewModel = new RankViewModel();

@Entry
@Component
struct RankPage {

  @State dataSource1: RankData[] = [];
  @State dataSource2: RankData[] = [];
  @State isSwitchDataSource: boolean = true; // 是否切换数据源

  // 初始化数据源
  aboutToAppear() {
    this.dataSource1 = rankModel.loadRankDataSource1();
    this.dataSource2 = rankModel.loadRankDataSource2();
  }

  build() {
    Column() {
      // 标题
      TitleComponent({ isRefreshData: $isSwitchDataSource, title: TITLE })
      // 列表头部
      ListHeaderComponent({
        paddingValue: {
          left: Style.RANK_PADDING,
          right: Style.RANK_PADDING
        },
        widthValue: Style.CONTENT_WIDTH
      })
        .margin({ // 外边距
          top: Style.HEADER_MARGIN_TOP,
          bottom: Style.HEADER_MARGIN_BOTTOM
        })
      // 排名列表
      this.RankList(Style.CONTENT_WIDTH)
    }
    .backgroundColor($r('app.color.background')) // 背景色
    .height(WEIGHT) // 高度
    .width(WEIGHT) // 宽度
  }

  @Builder RankList(widthValue: Length) {
    Column() {
      List() { // 列表
        ForEach(this.isSwitchDataSource ? this.dataSource1 : this.dataSource2,
          (item: RankData, index?: number) => {
            ListItem() {
              ListItemComponent({ index: (Number(index) + 1), name: item.name, stepNum: item.stepNum})
            }
          }, (item: RankData) => JSON.stringify(item))
      }
      .width(WEIGHT) // 宽度
      .height(Style.LIST_HEIGHT) // 高度
      .divider({ strokeWidth: Style.STROKE_WIDTH }) // 分割线
    }
    .padding({ // 内边距
      left: Style.RANK_PADDING,
      right: Style.RANK_PADDING
    })
    .borderRadius(Style.BORDER_RADIUS) // 边框半径
    .width(widthValue) // 宽度
    .alignItems(HorizontalAlign.Center) // 垂直居中对齐
    .backgroundColor(Color.White) // 背景色
  }
}

运行代码:
在这里插入图片描述
刷新列表:
在这里插入图片描述


http://www.niftyadmin.cn/n/5271524.html

相关文章

大数据技术之 Kettle(PDI)

Kettle 第一章 Kettle概述1.1、ETL简介1.2、Kettle简介1.3、作业 和 转换 概念1.4、核心组件1.5、下载安装 第二章 控件使用2.1、初体验:csv 转换 excel 示例2.2、转换2.2.1、输入控件2.2.1.1、表输入 2.2.2、输出控件2.2.2.1、表输出2.2.2.2、更新&插入/更新2.…

1852_bash中的find应用扩展

Grey 全部学习内容汇总: https://github.com/GreyZhang/toolbox 1852_bash中的find应用扩展 find这个工具我用了好多年了,但是是不是真的会用呢?其实不然,否则也不会出现这种总结式的笔记。其实,注意部分小细节之后…

开发人工智能APP需要多少成本?

人工智能是确保任何企业成功的强大资产。从医疗保健应用到客户支持,人工智能正在立即成为许多组织不可或缺的一部分。然而,在应用程序中实现人工智能可能会很昂贵。然而,构建生成式人工智能应用程序的成本取决于多种因素。 2023 年&#xff0…

Redis的四种部署模式:原理、优缺点及应用场景

Redis是一款高性能的开源NoSQL数据库,它支持多种数据结构,如字符串、列表、集合、散列、有序集合等。Redis的主要特点是将所有数据存储在内存中,实现了极高的读写速度,同时也提供了持久化机制,保证了数据的安全性和一致…

【算法Hot100系列】电话号码的字母组合

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Flink系列之:集合操作

Flink系列之:集合操作 一、集合操作二、UNION三、INTERSECT四、EXCEPT五、IN六、EXISTS 一、集合操作 适用于流、批操作 二、UNION UNION 和 UNION ALL 返回两个表中的数据。 UNION 会去重,UNION ALL 不会去重。 Flink SQL> create view t1(s) as…

利用腾讯微搭平台连接MYSQL

首先,找到控制台 然后再控制台搜索:微搭 然后点击进入: 这里如果是第一次进入,他应该会提示要创建环境。 然后按照这个步骤: 然后进入这个页面,点击编辑器: 然后在这里搜索表格: 点…

【idea】解决sprintboot项目创建遇到的问题

目录 一、报错Plugin ‘org.springframework.boot:spring-boot-maven-plugin:‘ not found 二、报错java: 错误: 无效的源发行版:17 三、java: 无法访问org.springframework.web.bind.annotation.CrossOrigin 四、整合mybatis的时候,报java.lang.Ill…