本案例介绍了如何实现一个简单的电子相册应用,主要功能包括:
- 实现首页顶部的轮播效果。
- 实现页面多种布局方式。
- 实现通过手势控制图片的放大、缩小、左右滑动查看细节等效果。
一、案例效果截图
二、案例运用到的知识点
- 核心知识点
- 组合手势:手势识别组,多种手势组合为复合手势,支持连续识别、并行识别和互斥识别。
- Swiper:滑块视图容器,提供子组件滑动轮播显示的能力。
- Grid:网格容器,由“行”和“列”分割的单元格所组成,通过指定“项目”所在的单元格做出各种各样的布局。
- Navigation:Navigation组件一般作为Page页面的根容器,通过属性设置来展示页面的标题、工具栏、菜单。
- List:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
- 其他知识点
- ArkTS 语言基础
- V2版状态管理:@ComponentV2/@Local/@Param/AppStorageV2
- 渲染控制:ForEach
- 自定义组件和组件生命周期
- 自定义构建函数@Builder
- @Extend:定义扩展样式
- Navigation导航组件与router路由导航
- 内置组件:Swiper/Stack/Column/Row/Image/Grid/List
- 常量与资源分类的访问
- MVVM模式
三、代码结构
├──entry/src/main/ets // 代码区
│ ├──common
│ │ ├──constansts
│ │ │ └──Constants.ets // 常量类
│ │ └──utils
│ │ └──Logger.ets // Logger公共类
│ ├──entryability
│ │ └──EntryAbility.ets // 程序入口类
│ ├──pages
│ │ ├──DetailListPage.ets // 图片详情页面
│ │ ├──DetailPage.ets // 查看大图页面
│ │ ├──IndexPage.ets // 电子相册主页面
│ │ └──ListPage.ets // 图片列表页面
│ └──view
│ └──PhotoItem.ets // 首页相册Item组件
└──entry/src/main/resources // 资源文件
四、公共文件与资源
本案例涉及到的常量类和工具类代码如下:
- 通用常量类
// entry/src/main/ets/common/constants/Constants.ets
export default class Constants {
/**
* banner list
*/
static readonly BANNER_IMG_LIST: Array<Resource> = [
$r('app.media.ic_scene_1'),
$r('app.media.ic_food_0'),
$r('app.media.ic_life_0'),
$r('app.media.ic_men_0')
]
/**
* scene list
*/
static readonly SCENE_LIST: Array<Resource> = [
$r('app.media.ic_scene_1'),
$r('app.media.ic_scene_2'),
$r('app.media.ic_scene_0')
]
/**
* men list
*/
static readonly MEN_LIST: Array<Resource> = [
$r('app.media.ic_men_0'),
$r('app.media.ic_men_2'),
$r('app.media.ic_men_3')
]
/**
* food list
*/
static readonly FOOD_LIST: Array<Resource> = [
$r('app.media.ic_food_1'),
$r('app.media.ic_food_0'),
]
/**
* life list
*/
static readonly LIFE_LIST: Array<Resource> = [
$r('app.media.ic_life_1'),
$r('app.media.ic_life_0'),
$r('app.media.ic_life_2'),
$r('app.media.ic_life_3'),
$r('app.media.ic_life_4'),
$r('app.media.ic_life_5')
]
/**
* index page img arr
*/
static readonly IMG_ARR: Resource[][] = [
new Array<Resource>().concat(Constants.SCENE_LIST, Constants.LIFE_LIST, Constants.MEN_LIST),
new Array<Resource>().concat(Constants.MEN_LIST, Constants.LIFE_LIST, Constants.SCENE_LIST),
new Array<Resource>().concat(Constants.FOOD_LIST, Constants.SCENE_LIST, Constants.SCENE_LIST),
new Array<Resource>().concat(Constants.LIFE_LIST, Constants.FOOD_LIST, Constants.MEN_LIST)
]
/**
* title font weight
*/
static readonly TITLE_FONT_WEIGHT: number = 500
/**
* aspect ratio
*/
static readonly BANNER_ASPECT_RATIO: number = 1.5
/**
* animate duration
*/
static readonly BANNER_ANIMATE_DURATION: number = 300
/**
* share delay
*/
static readonly SHARE_TRANSITION_DELAY: number = 100
/**
* aspect ratio
*/
static readonly STACK_IMG_RATIO: number = 0.7
/**
* item space
*/
static readonly LIST_ITEM_SPACE: number = 2
/**
* cache size
*/
static readonly CACHE_IMG_SIZE: number = 4
/**
* cache list
*/
static readonly CACHE_IMG_LIST: string[] = ['', '', '', '']
/**
* title
*/
static readonly PAGE_TITLE: string = '电子相册'
/**
* router param
*/
static readonly PARAM_PHOTO_ARR_KEY: string = 'photoArr'
/**
* selected index
*/
static readonly SELECTED_INDEX_KEY: string = 'selectedIndex'
/**
* grid column template
*/
static readonly GRID_COLUMNS_TEMPLATE: string = '1fr 1fr 1fr 1fr'
/**
* index page columns template
*/
static readonly INDEX_COLUMNS_TEMPLATE: string = '1fr 1fr'
/**
* percent
*/
static readonly FULL_PERCENT: string = '100%'
/**
* photo item percent
*/
static readonly PHOTO_ITEM_PERCENT: string = '90%'
/**
* show count
*/
static readonly SHOW_COUNT: number = 8
/**
* default width
*/
static readonly DEFAULT_WIDTH: number = 360
/**
* padding
*/
static readonly PHOTO_ITEM_PADDING: number = 8
/**
* offset
*/
static readonly PHOTO_ITEM_OFFSET: number = 16
/**
* item opacity offset
*/
static readonly ITEM_OPACITY_OFFSET: number = 0.2
/**
* double number
*/
static readonly DOUBLE_NUMBER: number = 2
/**
* list page url
*/
static readonly URL_LIST_PAGE: string = 'pages/ListPage'
/**
* detail list page url
*/
static readonly URL_DETAIL_LIST_PAGE: string = 'pages/ListDetailPage'
/**
* detail page url
*/
static readonly URL_DETAIL_PAGE: string = 'pages/DetailPage'
/**
* index page tag
*/
static readonly TAG_INDEX_PAGE: string = 'IndexPage push error '
/**
* list page tag
*/
static readonly TAG_LIST_PAGE: string = 'ListPage push error '
/**
* detail list page tag
*/
static readonly TAG_DETAIL_PAGE: string = 'DetailListPage push error '
}
2. 公共日志类
// entry/src/main/ets/common/utils/Logger.ets
import { hilog } from '@kit.PerformanceAnalysisKit'
const LOGGER_PREFIX: string = 'Electronic Album'
class Logger {
private domain: number
private prefix: string
private format: string = '%{public}s, %{public}s'
constructor(prefix: string = '', domain: number = 0xFF00) {
this.prefix = prefix
this.domain = domain
}
debug(...args: string[]): void {
hilog.debug(this.domain, this.prefix, this.format, args)
}
info(...args: string[]): void {
hilog.info(this.domain, this.prefix, this.format, args)
}
warn(...args: string[]): void {
hilog.warn(this.domain, this.prefix, this.format, args)
}
error(...args: string[]): void {
hilog.error(this.domain, this.prefix, this.format, args)
}
}
export default new Logger(LOGGER_PREFIX)
本案例涉及到的资源文件如下:
- string.json
// entry/src/main/resources/base/element/string.json
{
"string": [
{
"name": "module_desc",
"value": "模块描述"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "电子相册"
}
]
}
2. color.json
// entry/src/main/resources/base/element/color.json
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
},
{
"name": "detail_background",
"value": "#000000"
}
]
}
3. float.json
// entry/src/main/resources/base/element/float.json
{
"float": [
{
"name": "detail_list_margin",
"value": "20vp"
},
{
"name": "img_border_radius",
"value": "12vp"
},
{
"name": "grid_padding",
"value": "12vp"
},
{
"name": "title_padding",
"value": "24vp"
},
{
"name": "title_font_size",
"value": "30fp"
},
{
"name": "navi_bar_height",
"value": "56vp"
}
]
}
其他资源请到源码中获取。
五、应用首页
应用首页用Column组件来实现纵向布局,从上到下依次是标题组件Text、轮播图Swiper、相册列表Grid。标题和轮播图均设置固定高度,底部相册列表通过layoutWeight属性实现自适应布局占满剩余空间。
// entry/src/main/ets/pages/Index.ets
import { router } from '@kit.ArkUI'
import Constants from '../common/constants/Constants'
import PhotoItem from '../view/PhotoItem'
@Entry
@ComponentV2
struct IndexPage {
swiperController: SwiperController = new SwiperController()
scroller: Scroller = new Scroller()
@Local currentIndex: number = 0
@Local angle: number = 0
build() {
Column() {
Row() {
Text($r('app.string.EntryAbility_label'))
.fontSize($r('app.float.title_font_size'))
.fontWeight(Constants.TITLE_FONT_WEIGHT)
}
.height($r('app.float.navi_bar_height'))
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.Start)
.margin({ top: $r('app.float.grid_padding') })
.padding({ left: $r('app.float.title_padding') })
.width(Constants.FULL_PERCENT)
Swiper(this.swiperController) {
ForEach(Constants.BANNER_IMG_LIST, (item: Resource) => {
Row() {
Image(item)
.width(Constants.FULL_PERCENT)
.height(Constants.FULL_PERCENT)
}
.width(Constants.FULL_PERCENT)
.aspectRatio(Constants.BANNER_ASPECT_RATIO)
}, (item: Resource, index?: number) => JSON.stringify(item) + index)
}
.autoPlay(true)
.loop(true)
.margin($r('app.float.grid_padding'))
.borderRadius($r('app.float.img_border_radius'))
.clip(true)
.duration(Constants.BANNER_ANIMATE_DURATION)
.indicator(false)
Grid() {
ForEach(Constants.IMG_ARR, (photoArr: Array<Resource>) => {
GridItem() {
PhotoItem({ photoArr })
}
.width(Constants.FULL_PERCENT)
.aspectRatio(Constants.STACK_IMG_RATIO)
.onClick(() => {
router.pushUrl({
url: Constants.URL_LIST_PAGE,
params: { photoArr: photoArr }
})
})
}, (item: Resource, index?: number) => JSON.stringify(item) + index)
}
.scrollBar(BarState.Off)
.columnsTemplate(Constants.INDEX_COLUMNS_TEMPLATE)
.columnsGap($r('app.float.grid_padding'))
.rowsGap($r('app.float.grid_padding'))
.padding({
left: $r('app.float.grid_padding'),
right: $r('app.float.grid_padding')
})
.width(Constants.FULL_PERCENT)
.layoutWeight(1)
}
.width(Constants.FULL_PERCENT)
.height(Constants.FULL_PERCENT)
}
}
PhotoItem视图用来渲染相册图片:
// entry/src/main/ets/view/PhotoItem.ets
import Constants from '../common/constants/Constants'
@ComponentV2
export default struct PhotoItem {
@Param photoArr: Array<Resource> = []
@Local currentIndex: number = 0
private showCount: number = Constants.SHOW_COUNT / Constants.DOUBLE_NUMBER
@Builder albumPicBuilder(img: Resource, index: number) {
Column() {
Image(img)
.width(Constants.FULL_PERCENT)
.height(Constants.FULL_PERCENT)
.borderRadius($r('app.float.img_border_radius'))
.opacity(1 - (this.showCount - index - 1)
* Constants.ITEM_OPACITY_OFFSET)
}
.padding((this.showCount - index - 1) * Constants.PHOTO_ITEM_PADDING)
.offset({ y: (this.showCount - index - 1) * Constants.PHOTO_ITEM_OFFSET })
.height(Constants.PHOTO_ITEM_PERCENT)
.width(Constants.FULL_PERCENT)
}
build() {
Stack({ alignContent: Alignment.Top }) {
ForEach(Constants.CACHE_IMG_LIST, (image: string, index?: number) => {
if (index) {
this.albumPicBuilder(this.photoArr[this.showCount - index - 1], index)
}
}, (item: string, index?: number) => JSON.stringify(item) + index)
}
.width(Constants.FULL_PERCENT)
.height(Constants.FULL_PERCENT)
}
}
六、图片列表页
图片列表页是网格状展开的图片列表,主要使用Grid组件和GridItem组件,GridItem高度通过aspectRatio属性设置为跟宽度一致。
// entry/src/main/ets/pages/ListPage.ets
import { AppStorageV2, router } from '@kit.ArkUI'
import Constants from '../common/constants/Constants'
import { ListPageStorage } from '../model/ListPageModel'
@Entry
@ComponentV2
struct ListPage {
@Local storage: ListPageStorage = AppStorageV2.connect(
ListPageStorage,
'storage',
() => new ListPageStorage()
)!
photoArr: Array<Resource> = (router.getParams() as
Record<string, Array<Resource>>)[`${Constants.PARAM_PHOTO_ARR_KEY}`]
build() {
Navigation() {
Grid() {
ForEach(this.photoArr, (img: Resource, index?: number) => {
GridItem() {
Image(img)
.height(Constants.FULL_PERCENT)
.width(Constants.FULL_PERCENT)
.objectFit(ImageFit.Cover)
.onClick(() => {
if (!index) {
index = 0
}
this.storage.selectedIndex = index
router.pushUrl({
url: Constants.URL_DETAIL_LIST_PAGE,
params: {
photoArr: this.photoArr,
}
})
})
}
.width(Constants.FULL_PERCENT)
.aspectRatio(1)
}, (item: Resource) => JSON.stringify(item))
}
.scrollBar(BarState.Off)
.columnsTemplate(Constants.GRID_COLUMNS_TEMPLATE)
.rowsGap(Constants.LIST_ITEM_SPACE)
.columnsGap(Constants.LIST_ITEM_SPACE)
.layoutWeight(1)
}
.title(Constants.PAGE_TITLE)
.hideBackButton(false)
.titleMode(NavigationTitleMode.Mini)
}
}
上述代码中storage需要构建一个AppStorageV2的数据:
// entry/src/main/ets/model/ListPageModel.ets
@ObservedV2
export class ListPageStorage {
@Trace selectedIndex: number = 0
}
七、图片详情页
图片详情页由上下两个横向滚动的List组件完成整体布局,两个组件之间有联动的效果。上边展示的大图始终是底部List处于屏幕中间位置的图片。滚动或者点击底部的List,上边展示的大图会随着改变,同样左右滑动上边的图片时,底部List组件也会随之进行滚动。
界面跳转动画是通过共享元素实现的,通过给Image添加sharedTransition属性来实现共享元素转场动画。两个页面的组件配置为同一个shareId,当shareId被配置为空字符串时不会有共享元素转场效果。
// entry/src/main/ets/pages/ListDetailPage.ets
import { router,display, AppStorageV2 } from '@kit.ArkUI'
import Constants from '../common/constants/Constants'
import { ListPageStorage } from '../model/ListPageModel'
enum scrollTypeEnum {
STOP = 'onScrollStop',
SCROLL = 'onScroll'
}
@Entry
@ComponentV2
struct DetailListPage {
private smallScroller: Scroller = new Scroller()
private bigScroller: Scroller = new Scroller()
@Local deviceWidth: number = Constants.DEFAULT_WIDTH
@Local smallImgWidth: number = (this.deviceWidth - Constants.LIST_ITEM_SPACE
* (Constants.SHOW_COUNT - 1)) / Constants.SHOW_COUNT
@Local imageWidth: number = this.deviceWidth + this.smallImgWidth
private photoArr: Array<Resource | string> = (router.getParams() as
Record<string, Array<Resource | string>>)[`${Constants.PARAM_PHOTO_ARR_KEY}`]
private smallPhotoArr: Array<Resource | string>
= new Array<Resource | string>().concat(
Constants.CACHE_IMG_LIST,
(router.getParams() as Record<string, Array<Resource | string>>
)[`${Constants.PARAM_PHOTO_ARR_KEY}`],
Constants.CACHE_IMG_LIST)
@Local storage: ListPageStorage
= AppStorageV2.connect(
ListPageStorage, 'storage', () => new ListPageStorage())!
@Builder SmallImgItemBuilder(img: Resource, index?: number) {
if (index && index > (Constants.CACHE_IMG_SIZE - 1)
&& index < (this.smallPhotoArr.length - Constants.CACHE_IMG_SIZE)) {
Image(img)
.onClick(() => this.smallImgClickAction(index))
}
}
aboutToAppear() {
let displayClass: display.Display = display.getDefaultDisplaySync()
let width = displayClass?.width / displayClass.densityPixels
?? Constants.DEFAULT_WIDTH
this.deviceWidth = width
this.smallImgWidth = (width - Constants.LIST_ITEM_SPACE
* (Constants.SHOW_COUNT - 1)) / Constants.SHOW_COUNT
this.imageWidth = this.deviceWidth + this.smallImgWidth
}
onPageShow() {
this.smallScroller.scrollToIndex(this.storage.selectedIndex)
this.bigScroller.scrollToIndex(this.storage.selectedIndex)
}
goDetailPage(): void {
router.pushUrl({
url: Constants.URL_DETAIL_PAGE,
params: { photoArr: this.photoArr }
})
}
smallImgClickAction(index: number): void {
this.storage.selectedIndex = index - Constants.CACHE_IMG_SIZE
this.smallScroller.scrollToIndex(this.storage.selectedIndex)
this.bigScroller.scrollToIndex(this.storage.selectedIndex)
}
smallScrollAction(type: scrollTypeEnum): void {
this.storage.selectedIndex = Math.round(((
this.smallScroller.currentOffset().xOffset as number)
+ this.smallImgWidth / Constants.DOUBLE_NUMBER)
/ (this.smallImgWidth + Constants.LIST_ITEM_SPACE))
if (type === scrollTypeEnum.SCROLL) {
this.bigScroller.scrollTo({
xOffset: this.storage.selectedIndex * this.imageWidth, yOffset: 0
})
} else {
this.smallScroller.scrollTo({
xOffset: this.storage.selectedIndex * this.smallImgWidth, yOffset: 0
})
}
}
bigScrollAction(type: scrollTypeEnum): void {
let smallWidth = this.smallImgWidth + Constants.LIST_ITEM_SPACE
this.storage.selectedIndex = Math.round(((
this.bigScroller.currentOffset().xOffset as number) +
smallWidth / Constants.DOUBLE_NUMBER) / this.imageWidth)
if (type === scrollTypeEnum.SCROLL) {
this.smallScroller.scrollTo({
xOffset: this.storage.selectedIndex * smallWidth, yOffset: 0 })
} else {
this.bigScroller.scrollTo({
xOffset: this.storage.selectedIndex * this.imageWidth, yOffset: 0 })
}
}
build() {
Navigation() {
Stack({ alignContent: Alignment.Bottom }) {
List({
scroller: this.bigScroller,
initialIndex: this.storage.selectedIndex
}) {
ForEach(this.photoArr, (img: Resource) => {
ListItem() {
Image(img)
.height(Constants.FULL_PERCENT)
.width(Constants.FULL_PERCENT)
.objectFit(ImageFit.Contain)
.gesture(PinchGesture({ fingers: Constants.DOUBLE_NUMBER })
.onActionStart(() => this.goDetailPage()))
.onClick(() => this.goDetailPage())
}
.padding({
left: this.smallImgWidth / Constants.DOUBLE_NUMBER,
right: this.smallImgWidth / Constants.DOUBLE_NUMBER
})
.width(this.imageWidth)
}, (item: Resource) => JSON.stringify(item))
}
.onDidScroll((scrollOffset, scrollState) => {
if (scrollState === ScrollState.Fling) {
this.bigScrollAction(scrollTypeEnum.SCROLL)
}
})
.scrollBar(BarState.Off)
.onScrollStop(() => this.bigScrollAction(scrollTypeEnum.STOP))
.width(Constants.FULL_PERCENT)
.height(Constants.FULL_PERCENT)
.padding({ bottom: this.smallImgWidth * Constants.DOUBLE_NUMBER })
.listDirection(Axis.Horizontal)
List({
scroller: this.smallScroller,
space: Constants.LIST_ITEM_SPACE,
initialIndex: this.storage.selectedIndex
}) {
ForEach(this.smallPhotoArr, (img: Resource, index?: number) => {
ListItem() {
this.SmallImgItemBuilder(img, index)
}
.width(this.smallImgWidth)
.aspectRatio(1)
}, (item: Resource) => JSON.stringify(item))
}
.listDirection(Axis.Horizontal)
.onDidScroll((scrollOffset, scrollState) => {
if (scrollState === ScrollState.Fling) {
this.smallScrollAction(scrollTypeEnum.SCROLL)
}
})
.scrollBar(BarState.Off)
.onScrollStop(() => this.smallScrollAction(scrollTypeEnum.STOP))
.margin({
top: $r('app.float.detail_list_margin'),
bottom: $r('app.float.detail_list_margin')
})
.height(this.smallImgWidth)
.width(Constants.FULL_PERCENT)
}
.width(this.imageWidth)
.height(Constants.FULL_PERCENT)
}
.title(Constants.PAGE_TITLE)
.hideBackButton(false)
.titleMode(NavigationTitleMode.Mini)
}
}
八、查看大图页
查看大图页面由一个横向滚动的List组件来实现图片左右滑动时切换图片的功能,和一个Row组件实现图片的缩放和拖动查看细节功能。对图片进行缩放时会从List组件切换成Row组件来实现对单张图片的操作,对单张图片进行滑动操作时,也会由Row组件转换为List组件来实现图片的切换功能。
界面跳转动画是通过共享元素实现的,通过给Image添加sharedTransition属性来实现共享元素转场动画。两个页面的组件配置为同一个shareId,当shareId被配置为空字符串时不会有共享元素转场效果。
大图浏览界面双指捏合时通过改变Image组件的scale来控制图片的缩放,单手拖动时通过改变Image的偏移量来控制图片的位置,手势操作调用组合手势GestureGroup实现。其中PinchGesture实现双指缩放手势,PanGesture实现单指拖动手势。
// entry/src/main/ets/pages/DetailPage.ets
import { router, display, AppStorageV2 } from '@kit.ArkUI'
import Constants from '../common/constants/Constants'
import { ListPageStorage } from '../model/ListPageModel'
@Entry
@ComponentV2
struct DetailPage {
scroller: Scroller = new Scroller()
photoArr: Array<Resource> = (router.getParams() as
Record<string, Array<Resource>>)[`${Constants.PARAM_PHOTO_ARR_KEY}`]
preOffsetX: number = 0
preOffsetY: number = 0
currentScale: number = 1
@Local deviceWidth: number = Constants.DEFAULT_WIDTH
@Local smallImgWidth: number = (Constants.DEFAULT_WIDTH - Constants.LIST_ITEM_SPACE
* (Constants.SHOW_COUNT - 1)) /
Constants.SHOW_COUNT
@Local imageWidth: number = this.deviceWidth + this.smallImgWidth
@Local isScaling: boolean = true
@Local imgScale: number = 1
@Local imgOffSetX: number = 0
@Local imgOffSetY: number = 0
@Local bgOpacity: number = 0
@Local storage: ListPageStorage = AppStorageV2.connect(
ListPageStorage,
'storage',
() => new ListPageStorage()
)!
aboutToAppear() {
let displayClass: display.Display = display.getDefaultDisplaySync()
let width = displayClass?.width / displayClass.densityPixels
?? Constants.DEFAULT_WIDTH
this.deviceWidth = width
this.smallImgWidth = (width - Constants.LIST_ITEM_SPACE
* (Constants.SHOW_COUNT - 1)) / Constants.SHOW_COUNT
this.imageWidth = this.deviceWidth + this.smallImgWidth
}
resetImg(): void {
this.imgScale = 1
this.currentScale = 1
this.preOffsetX = 0
this.preOffsetY = 0
}
handlePanEnd(): void {
let initOffsetX = (this.imgScale - 1) * this.imageWidth
+ this.smallImgWidth
if (Math.abs(this.imgOffSetX) > initOffsetX) {
if (this.imgOffSetX > initOffsetX && this.storage.selectedIndex > 0) {
this.storage.selectedIndex -= 1
} else if (this.imgOffSetX < -initOffsetX
&& this.storage.selectedIndex < (this.photoArr.length - 1)) {
this.storage.selectedIndex += 1
}
this.isScaling = false
this.resetImg()
this.scroller.scrollTo({ xOffset: this.storage.selectedIndex
* this.imageWidth, yOffset: 0 })
}
}
build() {
Stack() {
List({
scroller: this.scroller,
initialIndex: this.storage.selectedIndex
}) {
ForEach(this.photoArr, (img: Resource) => {
ListItem() {
Image(img)
.objectFit(ImageFit.Contain)
.onClick(() => router.back())
}
.gesture(GestureGroup(GestureMode.Exclusive,
PinchGesture({ fingers: Constants.DOUBLE_NUMBER })
.onActionStart(() => {
this.resetImg()
this.isScaling = true
this.imgOffSetX = 0
this.imgOffSetY = 0
})
.onActionUpdate((event?: GestureEvent) => {
if (event) {
this.imgScale = this.currentScale * event.scale
}
})
.onActionEnd(() => {
if (this.imgScale < 1) {
this.resetImg()
this.imgOffSetX = 0
this.imgOffSetY = 0
} else {
this.currentScale = this.imgScale
}
}), PanGesture()
.onActionStart(() => {
this.resetImg()
this.isScaling = true
})
.onActionUpdate((event?: GestureEvent) => {
if (event) {
this.imgOffSetX = this.preOffsetX + event.offsetX
this.imgOffSetY = this.preOffsetY + event.offsetY
}
})
))
.padding({
left: this.smallImgWidth / Constants.DOUBLE_NUMBER,
right: this.smallImgWidth / Constants.DOUBLE_NUMBER
})
.width(this.imageWidth)
}, (item: Resource) => JSON.stringify(item))
}
.onScrollStop(() => {
let currentIndex
= Math.round(((this.scroller.currentOffset().xOffset as number) +
(this.imageWidth / Constants.DOUBLE_NUMBER)) / this.imageWidth)
this.storage.selectedIndex = currentIndex
this.scroller.scrollTo({ xOffset: currentIndex
* this.imageWidth, yOffset: 0 })
})
.width(Constants.FULL_PERCENT)
.height(Constants.FULL_PERCENT)
.listDirection(Axis.Horizontal)
.visibility(this.isScaling ? Visibility.Hidden : Visibility.Visible)
Row() {
Image(this.photoArr[this.storage.selectedIndex])
.position({ x: this.imgOffSetX, y: this.imgOffSetY })
.scale({ x: this.imgScale, y: this.imgScale })
.objectFit(ImageFit.Contain)
.onClick(() => router.back())
}
.gesture(GestureGroup(GestureMode.Exclusive,
PinchGesture({ fingers: Constants.DOUBLE_NUMBER })
.onActionUpdate((event?: GestureEvent) => {
if (event) {
this.imgScale = this.currentScale * event.scale
}
})
.onActionEnd(() => {
if (this.imgScale < 1) {
this.resetImg()
this.imgOffSetX = 0
this.imgOffSetY = 0
} else {
this.currentScale = this.imgScale
}
}),
PanGesture()
.onActionStart(() => {
this.preOffsetX = this.imgOffSetX
this.preOffsetY = this.imgOffSetY
})
.onActionUpdate((event?: GestureEvent) => {
if (event) {
this.imgOffSetX = this.preOffsetX + event.offsetX
this.imgOffSetY = this.preOffsetY + event.offsetY
}
})
.onActionEnd(() => this.handlePanEnd())
))
.padding({
left: this.smallImgWidth / Constants.DOUBLE_NUMBER,
right: this.smallImgWidth / Constants.DOUBLE_NUMBER
})
.width(this.imageWidth)
.height(Constants.FULL_PERCENT)
.visibility(this.isScaling ? Visibility.Visible : Visibility.Hidden)
}
.offset({ x: -(this.smallImgWidth / Constants.DOUBLE_NUMBER) })
.width(this.imageWidth)
.height(Constants.FULL_PERCENT)
.backgroundColor($r('app.color.detail_background'))
}
}
九、路由页面配置
页面间通过路由跳转时,需要配置每个页面的源路径。
{
"src": [
"pages/IndexPage",
"pages/DetailPage",
"pages/ListPage",
"pages/ListDetailPage"
]
}
✋ 需要参加鸿蒙认证的请点击 鸿蒙认证链接