本文基于鸿蒙系统,用600行代码实现类似图库预览效果
新建MediaDialogUtil.ets工具类,然后在工程下import 导入使用即可
scss
import {MediaParams, MediaDialogUtil} from ./MediaDialogUtil.ets
build() {
Stack() {
Text('点击打开弹窗')
.border({
width: 10,
color: '#000000'
})
.onClick(() => {
const data: MediaParams[] = [
new MediaParams('https://img1.baidu.com/it/u=3858873910,2096255234&fm=253&fmt=auto&app=138&f=JPEG'),
new MediaParams('https://img2.huashi6.com/images/resource/thumbnail/2025/03/06/15612_3721456001.jpg'),
new MediaParams('https://gips1.baidu.com/it/u=4077989092,1759249013&fm=3074&app=3074&f=JPEG'),
];
MediaDialogUtil.openDialog(data);
})
}
.width('100%')
.height('100%')
}
MediaDialogUtil.ets工具类具体代码如下:
js
import { ComponentContent, promptAction } from '@kit.ArkUI'
import { common } from '@kit.AbilityKit';
import { display } from '@kit.ArkUI'
const DURATION_200 = 200;
const ONE_HUNDRED_PERCENT = '100%';
const BLACK_COLOR = '#000000';
const CONST_NUMBER_40 = 40;
const CONST_NUMBER_50 = 50;
const MAX_SCALE_TIMES = 3; // 图片最大放大倍数
const TAP_SCALE_AVOID_FACTOR = 1.2; // 双击放大时出现黑边避让系数
const WHITE_COLOR = '#ffffff';
export enum MEDIA_TYPE {
PHOTO = 1,
VIDEO = 2
}
enum BORDER_ENUM {
LEFT,
RIGHT,
TOP,
BOTTOM,
NORMAL
}
interface EventImage {
width: number,
height: number
}
function resetImage(item: MediaParams) {
item.translateCenterX = item.centerDistanceX;
item.translateCenterY = item.centerDistanceY;
item.translateX = 0;
item.translateY = 0;
item.horizonBorder = BORDER_ENUM.NORMAL;
item.verticalBorder = BORDER_ENUM.NORMAL;
item.scale = 1;
item.params?.update();
}
function tapScaleImage(item: MediaParams, event: GestureEvent, recover = false) {
if (item.imgWidth === 0 || item.imgHeight === 0) {
return;
}
animateToImmediately({
duration: 150
}, () => {
if (!item.params) {
return
}
if (item.scale < MAX_SCALE_TIMES && !recover) {
const displayX = event.fingerList[0].displayX ?? item.params.screenWidth / 2;
const displayY = event.fingerList[0].displayY ?? item.params.screenHeight / 2;
scaleImage(item, MAX_SCALE_TIMES, displayX, displayY, item.params, false);
// 进行中心点坐标矫正 防止出现黑边
const realWidth = item.imgWidth * MAX_SCALE_TIMES;
const realHeight = item.imgHeight * MAX_SCALE_TIMES;
const screenWidth = item.params.screenWidth;
const screenHeight = item.params.screenHeight;
const leftBorder = item.imgWidth / 2 + item.translateCenterX + item.translateX - realWidth / 2;
const rightBorder = leftBorder + realWidth;
if (leftBorder > 0) {
item.translateCenterX -= TAP_SCALE_AVOID_FACTOR * leftBorder;
}
if (rightBorder < screenWidth) {
item.translateCenterX += TAP_SCALE_AVOID_FACTOR * (screenWidth - rightBorder);
}
const topBorder = item.imgHeight / 2 + item.translateCenterY + item.translateY - realHeight / 2;
const bottomBorder = topBorder + realHeight;
if (topBorder > 0) {
item.translateCenterY -= TAP_SCALE_AVOID_FACTOR * topBorder;
}
if (bottomBorder < screenHeight) {
item.translateCenterY += TAP_SCALE_AVOID_FACTOR * (screenHeight - bottomBorder);
}
checkBorder(item, item.params);
} else {
// 还原
resetImage(item);
}
})
}
// 检测图片是否抵达屏幕边缘
function checkBorder(item: MediaParams, params: MediaDialogParams, refresh = true) {
const realWidth = item.imgWidth * item.scale;
const realHeight = item.imgHeight * item.scale;
const screenWidth = params.screenWidth;
const screenHeight = params.screenHeight;
item.horizonBorder = BORDER_ENUM.NORMAL;
item.verticalBorder = BORDER_ENUM.NORMAL;
if (realWidth > screenWidth) {
const leftBorder = item.imgWidth / 2 + item.translateCenterX + item.translateX - realWidth / 2;
const rightBorder = leftBorder + realWidth;
if (leftBorder >= 0) {
item.horizonBorder = BORDER_ENUM.LEFT;
} else if (rightBorder <= screenWidth) {
item.horizonBorder = BORDER_ENUM.RIGHT;
}
}
if (realHeight > screenHeight) {
const topBorder = item.imgHeight / 2 + item.translateCenterY + item.translateY - realHeight / 2;
const bottomBorder = topBorder + realHeight;
if (topBorder >= 0) {
item.verticalBorder = BORDER_ENUM.TOP;
} else if (bottomBorder <= screenHeight) {
item.verticalBorder = BORDER_ENUM.BOTTOM;
}
}
if (refresh) {
params.update();
}
}
function pinScaleImage(item: MediaParams, event: GestureEvent) {
if (!item.params) {
return;
}
let newScale = Math.min(MAX_SCALE_TIMES, item.pinScale * event.scale);
const displayX = (event.pinchCenterX ?? item.params.screenWidth / 2);
const displayY = (event.pinchCenterY ?? item.params.screenHeight / 2);
scaleImage(item, newScale, displayX, displayY, item.params);
}
function scaleImage(item: MediaParams, newScale: number, displayX: number, displayY: number,
params: MediaDialogParams, adapt = true) {
const realWidth = item.imgWidth * item.scale;
const realHeight = item.imgHeight * item.scale;
const screenWidth = params.screenWidth;
const screenHeight = params.screenHeight;
// 缩放中心点-不能超过图片范围
let scaleCenterX = Math.min(item.imgWidth, Math.max(displayX - item.centerDistanceX, 0));
let scaleCenterY = Math.min(item.imgHeight, Math.max(displayY - item.centerDistanceY, 0));
// 调整缩放中心点 双击放大无需调整
if (adapt) {
if (realWidth <= screenWidth) {
scaleCenterX = item.imgWidth / 2;
} else if (item.horizonBorder === BORDER_ENUM.LEFT) {
scaleCenterX = 0;
} else if (item.horizonBorder === BORDER_ENUM.RIGHT) {
scaleCenterX = item.imgWidth;
}
if (realHeight <= screenHeight) {
scaleCenterY = item.imgHeight / 2;
} else if (item.verticalBorder === BORDER_ENUM.TOP) {
scaleCenterY = 0;
} else if (item.verticalBorder === BORDER_ENUM.BOTTOM) {
scaleCenterY = item.imgHeight;
}
}
// 计算 中心点偏移量
const moveX = (item.imgWidth / 2 - scaleCenterX) * (newScale - item.scale);
const moveY = (item.imgHeight / 2 - scaleCenterY) * (newScale - item.scale);
// 中心点真实坐标
item.translateCenterX += moveX;
item.translateCenterY += moveY;
item.scale = newScale;
checkBorder(item, params, adapt);
}
function panMoveImage(item: MediaParams, event: GestureEvent) {
if (!item || !item.params) {
return;
}
const deltaX = event.offsetX; // X轴移动的距离
const deltaY = event.offsetY; // Y轴移动的距离
const realWidth = item.imgWidth * item.scale;
const realHeight = item.imgHeight * item.scale;
const screenWidth = item.params.screenWidth;
const screenHeight = item.params.screenHeight;
// 判断边界
if (realWidth > screenWidth) { // 宽度小于不移动
const leftBorder = item.imgWidth / 2 + item.translateCenterX + item.panStartX - realWidth / 2;
const rightBorder = leftBorder + realWidth;
item.translateX = item.panStartX + Math.min(-leftBorder, Math.max(deltaX, screenWidth - rightBorder));
}
if (realHeight > screenHeight) {
const topBorder = item.imgHeight / 2 + item.translateCenterY + item.panStartY - realHeight / 2;
const bottomBorder = topBorder + realHeight;
item.translateY = item.panStartY + Math.min(-topBorder, Math.max(deltaY, screenHeight - bottomBorder));
}
checkBorder(item, item.params);
}
function generateCommonGesture(item: MediaParams, params: MediaDialogParams) {
return [
new TapGestureHandler({ count: 2 })
.onAction((event: GestureEvent) => {
tapScaleImage(item, event);
}),
new TapGestureHandler({ count: 1 })
.onAction(() => {
params.close();
}),
new PinchGestureHandler({ distance: 1 })
.onActionStart(() => {
item.pinScale = item.scale;
params.disabledSwiper = true;
params.update();
})
.onActionUpdate((event: GestureEvent) => {
if (!item) {
return;
}
pinScaleImage(item, event);
})
.onActionEnd((event: GestureEvent) => {
if (item.scale < 1) {
tapScaleImage(item, event, true);
}
params.disabledSwiper = false;
params.update();
})
.onActionCancel(() => {
params.disabledSwiper = false;
params.update();
})
];
}
class EmptyModifierGlobal implements GestureModifier {
applyGesture(event: UIGestureEvent): void {
event.clearGestures();
}
}
class ImageGestureModifierGlobal implements GestureModifier {
item: MediaParams | null = null;
constructor(item: MediaParams) {
this.item = item;
}
applyGesture(event: UIGestureEvent): void {
if (!this.item || !this.item.params) {
return;
}
if (this.item.horizonBorder !== BORDER_ENUM.NORMAL) { // 当到达边界的时候、添加手势、代理
const normalGesture = generateCommonGesture(this.item, this.item.params);
normalGesture.push(new PanGestureHandler({ fingers: 1 })
.onActionStart(() => {
if (!this.item || !this.item.params || this.item.params.isDetermined) {
return;
}
this.item.panStartX = this.item.translateX;
this.item.panStartY = this.item.translateY;
})
.onActionUpdate((event: GestureEvent) => {
if (!this.item || !this.item.params) {
return;
}
const params = this.item.params;
if (!params.isDetermined) {
params.isDetermined = true;
const deltaX = event.offsetX; // X轴移动的距离
const deltaY = event.offsetY; // Y轴移动的距离
if ((this.item.horizonBorder === BORDER_ENUM.LEFT && deltaX < 0) ||
(this.item.horizonBorder === BORDER_ENUM.RIGHT && deltaX > 0) ||
(deltaX == 0 && deltaY !== 0)) { // 触发位移逻辑
params.shouldMovePic = true;
this.item.params.disabledSwiper = true;
params.update();
}
}
if (params.shouldMovePic) {
panMoveImage(this.item, event);
}
})
.onActionEnd(() => {
if (!this.item || !this.item.params) {
return;
}
// 还原swiper
this.item.params.disabledSwiper = false;
this.item.params.isDetermined = false;
this.item.params.shouldMovePic = false;
this.item.params.update();
})
.onActionCancel(() => {
if (!this.item || !this.item.params) {
return;
}
// 还原swiper
this.item.params.disabledSwiper = false;
this.item.params.isDetermined = false;
this.item.params.shouldMovePic = false;
this.item.params.update();
}))
event.addParallelGesture(new GestureGroupHandler({
mode: GestureMode.Exclusive,
gestures: normalGesture
}))
} else {
event.clearGestures();
}
}
}
class ImageGestureModifier implements GestureModifier {
item: MediaParams | null = null;
constructor(item: MediaParams) {
this.item = item;
}
applyGesture(event: UIGestureEvent): void {
if (!this.item || !this.item.params) {
return;
}
if (this.item.horizonBorder !== BORDER_ENUM.NORMAL) {
event.clearGestures();
return;
}
const normalGesture = generateCommonGesture(this.item, this.item.params);
if (this.item.scale > 1) { // 图片放大了、添加panGesture、预览
normalGesture.push(
new PanGestureHandler({
fingers: 1,
direction: PanDirection.All
})
.onActionStart(() => {
if (!this.item || !this.item.params) {
return;
}
this.item.panStartX = this.item.translateX;
this.item.panStartY = this.item.translateY;
this.item.params.disabledSwiper = true;
this.item.params.update();
})
.onActionUpdate((event: GestureEvent) => {
if (!this.item) {
return;
}
panMoveImage(this.item, event);
})
.onActionEnd(() => {
if (!this.item || !this.item.params) {
return;
}
this.item.params.disabledSwiper = false;
this.item.params.update();
})
.onActionCancel(() => {
if (!this.item || !this.item.params) {
return;
}
this.item.params.disabledSwiper = false;
this.item.params.update();
})
)
}
event.addGesture(new GestureGroupHandler({
mode: GestureMode.Exclusive,
gestures: normalGesture
}))
}
}
export class MediaParams {
type = MEDIA_TYPE.PHOTO;
url = '';
scale = 1;
pinScale = 1; // 记录pinGesture开始时的scale
centerDistanceX = 0; // 图片中心点与手机屏幕中心点的距离
centerDistanceY = 0;
translateX = 0;
translateY = 0;
translateCenterX = 0; // 图片translate (本质上是中心点的位移动)
translateCenterY = 0;
panStartX = 0; // 记录panGesture开始的translateCenterX
panStartY = 0; // 记录panGesture开始的translateCenterY
imgWidth: number = 0;
imgHeight: number = 0;
params: MediaDialogParams | null = null;
horizonBorder = BORDER_ENUM.NORMAL;
verticalBorder = BORDER_ENUM.NORMAL;
modifier: ImageGestureModifier | null = null;
constructor(url: string) {
this.url = url;
}
}
export class MediaDialogParams {
isShow = true;
mediaData: MediaParams[] = [];
isFullScreen: boolean = false;
index: number = 0;
close: (immediate?: boolean) => void; // 关闭方法
swiperController: SwiperController = new SwiperController();
update: (param?: MediaDialogParams) => void; // 更新节点方法
screenWidth: number = 0; // 屏幕宽度、单位vp
screenHeight: number = 0;
disabledSwiper = false;
isDetermined = false; // 决定是图片位移还是swiper移动
shouldMovePic = false;
constructor(mediaData: MediaParams[], close: (immediate?: boolean) => void,
update: (param?: MediaDialogParams) => void, screenWidth: number, screenHeight: number) {
mediaData.forEach((item) => {
item.params = this;
if (item instanceof MediaParams) {
item.modifier = new ImageGestureModifier(item);
}
this.mediaData.push(item);
})
this.close = close;
this.update = update;
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
}
}
@Builder
function MediaNavigation(params: MediaDialogParams) {
Row() {
Text('').width(CONST_NUMBER_40).height(CONST_NUMBER_40) // 布局占位节点
Text(`${params.index + 1}/${params.mediaData.length}`).fontColor(WHITE_COLOR).fontSize(20)
Image('').width(CONST_NUMBER_40)
.height(CONST_NUMBER_40)
.onClick(() => {
params.close();
})
}
.width(ONE_HUNDRED_PERCENT)
.backgroundColor(Color.Transparent)
.justifyContent(FlexAlign.SpaceBetween)
.margin({
top: CONST_NUMBER_50,
left: CONST_NUMBER_40,
right: CONST_NUMBER_40
})
.hitTestBehavior(HitTestMode.None)
}
// 图片加载完成更新图片info
function updateImageInfo(item: MediaParams, event?: EventImage) {
item.imgWidth = px2vp(event?.width ?? 0);
item.imgHeight = px2vp(event?.height ?? 0);
if (item.imgWidth === 0 || item.imgHeight === 0 || !item.params) {
return;
}
// 根据ImageFit计算图片的真实宽高
let aspectRatio = item.imgWidth / item.imgHeight;
let screenAspectRation = item.params.screenWidth / item.params.screenHeight;
if (aspectRatio >= screenAspectRation) { // 宽度铺满
item.imgWidth = item.params.screenWidth ?? 0;
item.imgHeight = (item.params.screenWidth ?? 0) / aspectRatio;
} else { // 高度铺满
item.imgWidth = (item.params.screenHeight ?? 0) * aspectRatio;
item.imgHeight = item.params.screenHeight ?? 0
}
// 计算中心点距离
item.centerDistanceX = (item.params.screenWidth - item.imgWidth) / 2;
item.centerDistanceY = (item.params.screenHeight - item.imgHeight) / 2;
item.translateCenterX = item.centerDistanceX;
item.translateCenterY = item.centerDistanceY;
item.translateX = 0;
item.translateY = 0;
item.params.update();
}
@Builder
function MediaDialog(params: MediaDialogParams) {
if (params.isShow) {
Stack({ alignContent: Alignment.Top }) {
Swiper(params.swiperController) {
ForEach(params.mediaData, (item: MediaParams) => {
Stack({ alignContent: Alignment.TopStart }) {
Image(item.url)
.onComplete((event?: EventImage) => {
updateImageInfo(item, event);
})
.syncLoad(true)
.objectFit(ImageFit.Fill)
.width(item.imgWidth)
.height(item.imgHeight)
.scale({
x: item.scale,
y: item.scale,
})
.translate({
x: item.translateX + item.translateCenterX,
y: item.translateY + item.translateCenterY
})
}
.width(ONE_HUNDRED_PERCENT)
.height(ONE_HUNDRED_PERCENT)
.gestureModifier(item.modifier)
.clip(true)
}, (item: MediaParams, index: number) => {
return item.url + '_' + index + '_' + item.imgWidth + '_' + item.imgHeight;
})
}
.index(params.index)
.loop(false)
.indicator(false)
.autoPlay(false)
.width(ONE_HUNDRED_PERCENT)
.height(ONE_HUNDRED_PERCENT)
.disableSwipe(params.disabledSwiper)
.onChange((index: number) => {
const current = params.mediaData[params.index];
resetImage(current);
params.index = index;
params.isFullScreen = false;
params.disabledSwiper = false;
params.isDetermined = false;
params.shouldMovePic = false;
params.update();
})
if (!params.isFullScreen) {
MediaNavigation(params)
}
}
.width(ONE_HUNDRED_PERCENT)
.height(ONE_HUNDRED_PERCENT)
.backgroundColor(BLACK_COLOR)
.transition(
TransitionEffect.asymmetric(
TransitionEffect.opacity(1).combine(TransitionEffect.scale({
x: 0,
y: 0
})).animation({
duration: DURATION_200
}),
TransitionEffect.opacity(0).combine(TransitionEffect.scale({
x: 0,
y: 0
})).animation({
duration: DURATION_200
}),
))
.gestureModifier(
params.mediaData[params.index] instanceof MediaParams ?
new ImageGestureModifierGlobal(params.mediaData[params.index] as MediaParams) : new EmptyModifierGlobal()
)
} else {
Column() {
}.onAppear(() => {
// TransitionEffect 的退场动销无法触发onFinish、使用animateTo触发
animateTo({
duration: DURATION_200,
onFinish: () => {
params.close(true);
}
}, () => {
})
})
}
}
export class MediaDialogUtil {
private constructor() {
}
public static openDialog(data: MediaParams[]): void {
const context = (getContext()) as common.UIAbilityContext
const mainWin = context.windowStage.getMainWindowSync()
const uiContext = mainWin.getUIContext()
const promptAction = uiContext.getPromptAction();
const screenWidth = px2vp(display.getDefaultDisplaySync().width)
const screenHeight = px2vp(display.getDefaultDisplaySync().height)
const dialogParams = new MediaDialogParams(
data,
(immediate = false) => {
if (immediate) { // 立马关闭
promptAction.closeCustomDialog(contentNode);
} else {
dialogParams.isShow = false;
contentNode.update(dialogParams);
}
},
(params?: MediaDialogParams) => {
contentNode.update(params ?? dialogParams);
},
screenWidth,
screenHeight
);
// 创建弹窗组件
const contentNode = new ComponentContent(
uiContext,
wrapBuilder(MediaDialog),
dialogParams
);
const options: promptAction.BaseDialogOptions = {
alignment: DialogAlignment.Bottom,
isModal: true,
autoCancel: false,
maskColor: Color.Transparent,
transition: TransitionEffect.IDENTITY
}
promptAction.openCustomDialog(contentNode, options);
}
}