#2023盲盒+码# 通过商城应用熟悉常用控件用法 原创
【本文正在参加 2023「盲盒」+码有奖征文活动】,活动链接 https://ost.51cto.com/posts/25284
应用简介
本应用是在Huawei的codelabs上的购物类应用的基础上开发的一个程序,只是添加并丰富了一些内容,对改动做一些简要的介绍,主要是通过实践熟悉一下常用控件的用法。
效果预览
简要介绍
1.点击列表项跳转到对应详情Page页
通过在ListItem中添加onClick事件,实现点击跳转的目的,在页面跳转时传递want参数,里面包括包名称、页面名称和一些参数。
ListItem() {
...
}.onClick(() => {
let handler = getContext(this) as AppContext.Context;
viewModel.startDetailsAbility(handler, item?.position);
})
public startDetailsAbility(context, index: number): void {
const want = {
bundleName: getContext(context).applicationInfo.name,
abilityName: DETAILS_ABILITY_NAME,
parameters: {
position: index
}
};
try {
context.startAbility(want);
} catch (error) {
hilog.error(HOME_PAGE_DOMAIN, TAG, '%{public}s', error);
}
}
2.详情Page页可以上下滑动
页面的上下滑动,是通过scroll组件来完成的,通常把单屏显示不下的,需要滑动显示的内容,先通过column组合在一起,然后再放置到scroll里面。
build() {
Column() {
Scroll() {
Column() {
Stack({ alignContent: Alignment.Top }) {
// GoodsPreviewer displays images about goods.
PreviewerComponent({ goodsImages: this.goodsDetails.goodsImages })
this.TopBarLayout()
}
.height(DetailsPageStyle.TOP_LAYOUT_HEIGHT)
.width(PERCENTAGE_100)
.backgroundColor($r('app.color.background1'))
// the card layout style about goods information.
this.CardsLayout()
}.width(PERCENTAGE_100)
}
.height(DetailsPageStyle.SCROLL_LAYOUT_WEIGHT)
.backgroundColor($r('app.color.background'))
// tool bar in the bottom.
BottomBarComponent().height(DetailsPageStyle.TOOLBAR_WEIGHT)
}
.height(PERCENTAGE_100)
.width(PERCENTAGE_100)
}
3.点击左上角返回按钮,返回到列表页
左上角的返回,实际是一个图片组件,点击后关闭当前页面自然就显示之前的页面了。
@Builder BackLayout() {
Image($rawfile('detail/detail_back.png'))
.setTopImageStyle()
.onClick(() => {
let handler = getContext(this) as AppContext.UIAbilityContext;
handler.terminateSelf();
})
}
4.Swiper显示4张图片
4张图片路径是放置在一个数组中,通过ForEach加载到Swiper容器组件里,
Swiper(this.swiperController) {
ForEach(this.goodsImages, (item: Resource) => {
// Text(item).width('90%').height(160).backgroundColor(0xAFEEEE).textAlign(TextAlign.Center).fontSize(30)
Image(item)
.objectFit(ImageFit.Fill)
.height("100%")
.width("100%")
.borderRadius(12)
.align(Alignment.Center)
}, item => JSON.stringify(item))
}
.cachedCount(2)
.index(0)
.autoPlay(true)
.interval(2000)
.indicator(true)
.loop(true)
.duration(500)
.itemSpace(0)
.curve(Curve.Linear)
.onChange((index: number) => {
console.info(index.toString())
this.indicator = index + 1
})
5.商品价格、描述等和点击的列表项一致
商品价格保持一致,实现方法是靠startAbility时传递参数,打开DetailsAbility 时,调用了DetailsAbility
.ts的onCreate函数,在其中有AppStorage.SetOrCreate(KEY, index)的调用,这样就把position参数保存为应用全局状态,
在DetailPage.ets中通过let position = AppStorage.Get<number>(KEY);获取具体位置,进而viewModel获得对应详细数据
this.goodsDetails = viewModel.loadDetails(position);
价钱和其他的具体信息就都能获得了。
const want = {
bundleName: getContext(context).applicationInfo.name,
abilityName: DETAILS_ABILITY_NAME,
parameters: {
position: index
}
};
try {
context.startAbility(want);
} catch (error) {
hilog.error(HOME_PAGE_DOMAIN, TAG, '%{public}s', error);
}
export default class DetailsAbility extends UIAbility {
onCreate(want, launchParam) {
let index: number = want?.parameters?.position;
AppStorage.SetOrCreate(KEY, index);
hilog.info(DETAIL_ABILITY_DOMAIN, TAG, '%{public}s', 'Ability onCreate');
}
...
const KEY: string = 'GoodsPosition';
let position = AppStorage.Get<number>(KEY);
aboutToAppear() {
this.goodsDetails = viewModel.loadDetails(position);
this.commentCount = this.goodsDetails.comment_count;
this.commentPercent = this.goodsDetails.comment_percent;
}
6.用户评价通过List+懒加载的方式展示
当列表中数据量较大时,一般推荐使用懒加载模式,
List({ space: CommentList.SPACE }) {
LazyForEach(this.commentsListData, (item) => {
ListItem() {
Column() {
Row() {
...
}
}
}
})
}
.width(PERCENTAGE_100)
.backgroundColor(Color.White)
.edgeEffect(EdgeEffect.None)
第一个参数this.commentsListData是需要进行数据迭代的数据源,
@Component
export default struct CommentsComponent {
@Provide commentsListData: CommentDataSource = new CommentDataSource();
export class CommentDataSource extends BasicDataSource {
private listData = createListRange();
public totalCount(): number {
return this.listData.length;
}
public getData(index: number): CommentItemType {
return this.listData[index];
}
}
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = []
public totalCount(): number {
return 0;
}
public getData(index: number): CommentItemType {
return undefined;
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const position = this.listeners.indexOf(listener);
if (position >= 0) {
this.listeners.splice(position, 1);
}
}
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
})
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
})
}
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
})
}
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
})
}
}
7.完成ArkTS卡片
添加卡片,直接在工程中添加一个ServiceWidget就可以了,然后进行页面布局,在根布局中添加以下代码就可以打开主应用了。
.onClick(() => {
postCardAction(this, {
"action": this.ACTION_TYPE,
"abilityName": this.ABILITY_NAME,
"params": {
"message": this.MESSAGE
}
});
})
代码分析
1.页面跳转
页面间跳转是通过startAbility实现的,同时还可以传递一个want参数,便于携带一些变量的值。
const want = {
bundleName: getContext(context).applicationInfo.name,
abilityName: DETAILS_ABILITY_NAME,
parameters: {
position: index
}
};
try {
context.startAbility(want);
} catch (error) {
hilog.error(HOME_PAGE_DOMAIN, TAG, '%{public}s', error);
}
此处的context实际是getContext(this) as AppContext.Context;
2.组件间参数传递
参数传递通过在父组件中设置@State装饰器修饰变量,在子组件中设置@prop装饰的变量,然后调用子组件时通过参数传递。
父组件
@Entry
@Component
struct DetailsPage {
private goodsDetails: GoodsListItemType;
@State commentCount: string = "";
@State commentPercent: string = "";
aboutToAppear() {
this.goodsDetails = viewModel.loadDetails(position);
this.commentCount = this.goodsDetails.comment_count;
this.commentPercent = this.goodsDetails.comment_percent;
}
子组件
@Component
export default struct CommentsHeaderComponent {
@Prop commentCount: string;
@Prop commentPercent: string;
组件调用
CardComponent() {
CommentsHeaderComponent({ commentCount: this.commentCount, commentPercent: this.commentPercent })
CommentsComponent().margin({ top: DetailsPageStyle.COMMENT_LIST_MARGIN_TOP })
}
3.扩展样式的选择,@Extend还是@Style?
针对组件属性反复设置,系统给了解决方案,可以通过扩展样式写到一个函数里,实现多次复用,避免了大量代码重复设置。但@Style只支持通用属性的扩展,而@Extend可以扩展原生组件的样式。
例如,以下的扩展只能用Extend,因为里面的样式只是针对Text组件的,和其他组件没有通用性。
@Extend(Text) function commentText () {
.lineHeight(CommentHeader.LINE_HEIGHT)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.textCase(TextCase.UpperCase)
.fontSize(AppFontSize.MIDDLE)
.fontColor($r('app.color.text'))
.fontWeight(AppFontWeight.BOLDER)
.textAlign(TextAlign.Start)
}
又比如,这里的Extend实际也可以用Style代替。
@Extend(Image) function setTopImageStyle() {
.width(DetailsPageStyle.TOP_IMAGE_SIZE)
.height(DetailsPageStyle.TOP_IMAGE_SIZE)
}
@Styles function setTopImageStyle() {
.width(DetailsPageStyle.TOP_IMAGE_SIZE)
.height(DetailsPageStyle.TOP_IMAGE_SIZE)
}
总结
通过这次操作实践,不仅复习了常用组件的基本用法,而且还对一些新技能有了深入的了解,例如,页面间携带参数跳转,懒加载方式的实现以及应用全局的状态管理方法等。