Быстрая поддержка списков, таблиц, водопадных потоков и загрузки дополнительных данных при прокрутке вниз.
![]() |
![]() |
---|---|
![]() |
![]() |
ohpm install @candies/loading_more_list
У нас есть 7 различных состояний для одного списка.
export enum IndicatorStatus {
none, // Инициализированное состояние
loadingMoreBusying, // Загрузка дополнительных данных
fullScreenBusying, // Полноэкранный прогресс загрузки до первой загрузки данных
loadingMoreError, // Ошибка загрузки дополнительных данных
fullScreenError, // Полноэкранный прогресс загрузки с ошибкой
noMoreLoad, // Больше данных нет
empty // Список пуст
}
```Эти состояния можно разделить на три основных сценария:
1. Инициализированное состояние
* `none`
2. Состояние до первой загрузки данных
* `fullScreenBusying`
* `fullScreenError`
* `empty`
3. Состояния при наличии данных
* `loadingMoreBusying`
* `loadingMoreError`
* `noMoreLoad`
Эти три состояния рисуются путем добавления элемента в конец данных.
Ключевые методы находятся в `LoadingMoreBase` - `totalCount` и `getData`.
```typescript
lastItemIsLoadingMoreItem: boolean = true;
totalCount(): number {
return this.length + (this.lastItemIsLoadingMoreItem ? 1 : 0);
}
Для реализации источника данных с подгрузкой необходимо наследовать LoadingMoreBase<T>
. Перегрузите метод loadData
для загрузки данных. Не забудьте устанавливать hasMore
в false
, когда данных больше нет.
Вот пример источника данных:
refresh
для инициализации начальных значений. Этот метод можно использовать для обновления всего списка при выполнении действия "отскролить вниз".loadData
для предоставления логики загрузки данных и проверки наличия дополнительных данных. Если загрузка прошла успешно, используйте this.addAll
для добавления новых данных и верните true
. Если загрузка не удалась, верните false
.import {
LoadingMoreBase,
} from '@candies/loading_more_list'
import { FeedList, TuChongSource } from './TuChongSource';
import http from '@ohos.net.http';
export class TuChongRepository extends LoadingMoreBase<FeedList> {
public hasMore: boolean = true;
page: number = 1;
}
``````## Использование
### Импорт ссылок
```typescript
import {
LoadingMoreList,
LoadingMoreBase,
IndicatorWidget,
IndicatorStatus,
} from '@candies/loading_more_list'
LoadingMoreList
— это наш компонент для загрузки дополнительных элементов. Его параметры следующие:
/// Список, может быть List, Grid или WaterFlow
@BuilderParam
private builder: () => void;
/// Источник данных для списка
@Link sourceList: LoadingMoreBase<any>;
/// Конструктор состояния индикатора, только для [IndicatorStatus.fullScreenBusying,IndicatorStatus.fullScreenError,IndicatorStatus.empty]
@BuilderParam
indicatorBuilder?: ($$: IndicatorParam) => void = this.buildIndicator;
Подготовим простой источник данных.
import {
LoadingMoreBase,
} from '@candies/loading_more_list'
``````typescript
export class ListData extends LoadingMoreBase<number> {
hasMore: boolean = true;
pageSize: number = 10;
maxCount: number = 20;
public async refresh(notifyStateChanged: boolean = false): Promise<boolean> {
this.hasMore = true;
return super.refresh(notifyStateChanged);
}
async loadData(isLoadMoreAction: boolean): Promise<boolean> {
// Моделируем задержку запроса на 1 секунду
return new Promise<boolean>((resolve) => {
setTimeout(() => {
var length = this.length;
let list = [];
for (let index = length; index < length + this.pageSize; index++) {
list.push(index);
}
this.addAll(list);
if (this.length >= this.maxCount) {
this.hasMore = false;
}
resolve(this.isSuccess);
}, 1000);
});
}
}
Мы должны учитывать только случаи, когда элементы списка превышают его длину. Если при построении элементов списка обнаруживается, что это LoadingMoreItem
, то можно использовать следующий метод для создания соответствующего состояния UI
.
if (this.listData.isLoadingMoreItem(item))
IndicatorWidget({
indicatorStatus: this.listData.getLoadingMoreItemStatus(item),
sourceList: this.listData,
})
Полный код представлен ниже:
import { LoadingMoreList, IndicatorWidget } from '@candies/loading_more_list'
import { ListData } from '../data/ListData';
@Entry
@Component
struct ЗагрузкаДополнительныхЭлементовDemo {
@State listData: ListData = new ListData();
}
``` @Builder
построитьСписок() {
Список() {
ЛениваяПоследовательность(this.listData, (элемент, индекс) => {
ЭлементСписка() {
// индекс == this.listData.length
если (this.listData.загрузкаДополнительныхЭлементов(элемент))
ИндикаторВиджет({
статусИндикатора: this.listData.статусЗагрузкиДополнительныхЭлементов(элемент),
исходныйСписок: this.listData,
})
иначе
Текст(`${элемент}`,).выравнивание(Выравнивание.Центр).высота(100)
}.ширина('100%')
},
(элемент, индекс) => {
вернуть `${элемент}`
}
)
}
.растяжение(1)
.наДостижениеКонца(() => {
this.listData.загрузкаДополнительныхЭлементов();
})
} })
}
построить() {
Навигация() {
ЗагрузкаДополнительныхЭлементов({
исходныйСписок: this.listData,
построитель: () => this.построитьСписок(),
})
}
.заголовок('ЗагрузкаДополнительныхЭлементовDemo').режимЗаголовкаНавигации(НавигацияЗаголовокРежим.Мини)
}
}
Когда в Список
вызывается наДостижениеКонца
обратный вызов, вы можете вручную вызвать метод загрузкаДополнительныхЭлементов
. Конечно, вы также можете не вызывать его вручную, так как ЗагрузкаДополнительныхБаза
уже автоматически вызывает его. Разница заключается в том, что если загрузка дополнительных элементов не удалась, вы добавили этот вручную вызов, вы можете использовать прокрутку вверх, чтобы снова вызвать загрузку дополнительных элементов. В противном случае, вы можете использовать, например, нажатие, чтобы снова вызвать загрузкаДополнительныхЭлементов
.
Аналогично списку, но обратите внимание, что построение последнего элемента немного отличается. Вам нужно установить columnStart
и columnEnd
для последнего элемента GridItem
, чтобы сделать его кросс-колонкой, что позволит ему занимать всю строку (конечно, это обычно, вы можете настроить его в зависимости от ваших потребностей).
import { ЗагрузкаДополнительныхЭлементов, ЗагрузкаДополнительныхБаза, ИндикаторВиджет } from '@candies/загрузка_дополнительных_элементов'
import { ListData } from '../data/ListData';
```@Entry
@Component
struct ЗагрузкаДополнительныхЭлементовGridDemo {
@State listData: ListData = new ListData();
передПоявлением() {
this.listData.размерСтраницы = 50;
this.listData.максимальноеКоличество = 100;
}
```typescript
@Builder
buildList() {
Grid() {
LazyForEach(this.listData, (item, index) => {
// index == this.listData.length
if (this.listData.isLoadingMoreItem(item))
GridItem() {
IndicatorWidget({
indicatorStatus: this.listData.getLoadingMoreItemStatus(item),
sourceList: this.listData,
})
}
// loading more item занимает одну строку, вы можете определить это в зависимости от вашего случая
.columnStart(0).columnEnd(4)
else
GridItem() {
Text(`${item}`,).align(Alignment.Center)
}.height(100).width('100%')
},
(item, index) => {
return `${item}`
}
)
}
.flexGrow(1)
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
}
// api10
// .onReachEnd(() => {
// this.listData.loadMore();
//
// })
}
build() {
Navigation() {
LoadingMoreList({
sourceList: this.listData,
builder: () => this.buildList(),
})
}
.title('Загрузка дополнительных элементов').titleMode(NavigationTitleMode.Mini)
}
Официальный водопад предоставляет footer
обратный вызов, который можно использовать для создания стиля последнего элемента.
Сначала нам нужно установить lastItemIsLoadingMoreItem
в false
.
this.listData.lastItemIsLoadingMoreItem = false;
Затем используем footer
обратный вызов для создания компонента для загрузки дополнительных элементов.```typescript
@Builder
buildFooter() {
if (!this.listData.hasMore) {
IndicatorWidget({
indicatorStatus: IndicatorStatus.noMoreLoad,
});
} else if (this.listData.indicatorStatus === IndicatorStatus.loadingMoreError) {
IndicatorWidget({
indicatorStatus: IndicatorStatus.loadingMoreError,
sourceList: this.listData,
});
} else {
IndicatorWidget({
indicatorStatus: IndicatorStatus.loadingMoreBusying,
});
}
}
import { LoadingMoreList, IndicatorWidget, IndicatorStatus } from '@candies/loading_more_list'
import { TuChongRepository } from '../data/TuChongRepository';
import { FeedList } from '../data/TuChongSource';
@Entry
@Component
struct LoadingMoreWaterFlowDemo {
@State listData: TuChongRepository = new TuChongRepository();
aboutToAppear() {
this.listData.lastItemIsLoadingMoreItem = false;
}
@Builder
buildFooter() {
if (!this.listData.hasMore) {
IndicatorWidget({
indicatorStatus: IndicatorStatus.noMoreLoad,
});
} else if (this.listData.indicatorStatus === IndicatorStatus.loadingMoreError) {
IndicatorWidget({
indicatorStatus: IndicatorStatus.loadingMoreError,
sourceList: this.listData,
});
} else {
IndicatorWidget({
indicatorStatus: IndicatorStatus.loadingMoreBusying,
});
}
}
}
``` @Builder
buildList() {
WaterFlow({
footer: () => this.buildFooter()
}) {
LazyForEach(this.listData, (item, index) => {
FlowItem() {
TuChongImageListItem({ item: item, index: index })
}
},
(item, index) => {
var feedList = item as FeedList;
if ('post_id' in feedList) {
return `${feedList.post_id}`;
}
return `${item}`;
}
)
}
.columnsTemplate("1fr 1fr")
.columnsGap(10)
.rowsGap(5)
.flexGrow(1)
.onReachEnd(() => {
this.listData.loadMore();
})
}```markdown
build() {
Navigation() {
LoadingMoreList({
sourceList: this.listData,
builder: () => this.buildList(),
})
}
.title('LoadingMoreWaterFlowDemo').titleMode(NavigationTitleMode.Mini)
}
}
@Component
struct TuChongImageListItem {
item: FeedList;
index: number;
hasImage(): boolean {
return this.item.images.length !== 0;
}
imageUrl(): string {
if (!this.hasImage()) {
return '';
}
return `https://photo.tuchong.com/${this.item.images[0].user_id}/f/${this.item.images[0].img_id}.jpg`;
}
avatarUrl() {
return this.item.site.icon;
}
imageTitle() {
if (!this.hasImage()) {
return this.item.title;
}
return this.item.images[0].title;
}
}
``` imageDescription() {
if (!this.hasImage()) {
return this.item.content;
}
return this.item.images![0].description;
}
aboutToAppear() {
// вывод в консоль this.imageUrl
}
getAspectRatio(): number {
return this.item.images[0].width / this.item.images![0].height;
}
build() {
Column() {
if (this.hasImage())
Image(this.imageUrl())
.objectFit(ImageFit.Fill)
.autoResize(true)
.width('100%')
.aspectRatio(this.getAspectRatio())
.backgroundColor('#22808080')
Divider()
}
}
}
```
### Структурированный компонент состояния
Если мы не создаем структурированный компонент состояния, по умолчанию предоставляется `IndicatorWidget`, который создает `UI` для различных состояний.
Мы можем создать собственный компонент состояния `CustomIndicatorWidget` с помощью обратного вызова `indicatorBuilder` и обработки последнего элемента, чтобы создать пользовательский эффект состояния.
Полный код представлен ниже:
```typescript
import { LoadingMoreList, LoadingMoreBase, IndicatorStatus, IndicatorParam, } from '@candies/loading_more_list'
import { ListData } from '../data/ListData';
@Entry
@Component
struct LoadingMoreCustomIndicatorDemo {
@State listData: ListData = new ListData();
}
``` @Builder
buildList() {
List() {
LazyForEach(this.listData, (item, index) => {
ListItem() {
// index равно this.listData.length
if (this.listData.isLoadingMoreItem(item))
CustomIndicatorWidget({
indicatorStatus: this.listData.getLoadingMoreItemStatus(item),
sourceList: this.listData,
})
else
Text(`${item}`,).align(Alignment.Center).height(100)
}.width('100%')
},
(item, index) => {
return `${item}`
}
)
}
.flexGrow(1)
.onReachEnd(() => {
this.listData.loadMore(); })
}
@Builder
buildIndicator($$: IndicatorParam) {
CustomIndicatorWidget({ indicatorStatus: $$.indicatorStatus, sourceList: $$.sourceList, })
}
build() {
Navigation() {
LoadingMoreList({
sourceList: this.listData,
builder: () => this.buildList(),
indicatorBuilder: ($$: IndicatorParam) => this.buildIndicator(
{ indicatorStatus: $$.indicatorStatus, sourceList: $$.sourceList, }
),
})
}
.title('Загрузка дополнительных элементов с пользовательским индикатором').titleMode(NavigationTitleMode.Mini)
}
}@Component
export struct CustomIndicatorWidget {
/// Источник списка на основе [LoadingMoreBase].
indicatorStatus: IndicatorStatus;
sourceList: LoadingMoreBase<any>;
```markdown
build() {
if (this.indicatorStatus == IndicatorStatus.none)
Column()
else if (this.indicatorStatus == IndicatorStatus.fullScreenBusying)
Row() {
Text('Загрузка... Не торопитесь'),
LoadingProgress().width(50).height(50).margin({ left: 10 })
}.justifyContent(FlexAlign.Center).width('100%').height('100%')
else if (this.indicatorStatus == IndicatorStatus.fullScreenError)
Row() {
Text('Похоже, произошла ошибка? Нажмите для обновления')
}.justifyContent(FlexAlign.Center)
.width('100%').height('100%').onClick((event) => {
this.sourceList.errorRefresh();
})
else if (this.indicatorStatus == IndicatorStatus.empty)
Row() {
Text('Здесь только воздух!')
}.justifyContent(FlexAlign.Center).width('100%').height('100%')
else if (this.indicatorStatus == IndicatorStatus.loadingMoreBusying)
Row() {
Text('Загрузка... Не тяните слишком сильно'),
LoadingProgress().width(40).height(40).margin({ left: 10 })
}.justifyContent(FlexAlign.Center).width('100%').height(50).backgroundColor('#22808080')
else if (this.indicatorStatus == IndicatorStatus.loadingMoreError)
Row() {
Text('Сетевая связь нестабильна? Нажмите для повторной загрузки!')
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height(50)
.backgroundColor('#22808080')
.onClick((event) => {
this.sourceList.errorRefresh();
})
else if (this.indicatorStatus == IndicatorStatus.noMoreLoad)
Row() {
Text('Я уже на пределе, не тяните больше!')
}.justifyContent(FlexAlign.Center).width('100%').backgroundColor('#22808080').height(50)
else
Column()
}
}
```
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )