HarmonyOS 鸿蒙照片上墙功能分享
·
话不多说,先给大家看看实际效果图
。。。。gif 最大 5M,4s 的都 18M 了

算了,发个静态图吧,后面视频审批过了再加进来。


HarmonyOS 鸿蒙照片上墙功能分享
背景:
这个是最近正好做项目需求写的东西,感觉可能别的地方也用得到,又挺独立的就分享一下。
实现思路:
这是用四个 Image 来实现的,背景图和前景图各两个,如果都用一个来实现会切换的时候比较卡顿,不像现在这么流畅。
背景图就比较简单的透明度变换的动画,前景图是做了好多个动画组,然后随机抽出来下一张图用什么动画来展示。整体配合形成的这么个效果。
至于怎么做的切换,主要就是靠存储前台获得焦点的是哪个来切换的。位置其实没变,主要看哪个显示在前台。
完整代码:
这是组件本体,不包含导入代码,另外我是为了测试效果才写成了可跳转的页面,实际上是当组件用的。
下面我会把用得到的工具类也发出来。包能运行的。对了数据源得自己弄奥。
/**
* @文件名称: PhotoWayAnimationView.ets
* @路径: entry/src/main/ets/playsongs/photosway
* @作者: 魔眼天王
* @创建时间: 2026/1/16 14:31
* @描述: 照片上墙播放照片动画的组件
*/
@HMRouter({ pageUrl: PageConstant.PHOTOS_ON_THE_WAY1 })
@Component
export struct PhotoWayAnimationView {
@State animate: boolean = false;
//背景层
@State imgBGUrl1: string | null = null
@State imgBGUrl2: string | null = null
@State activeBg: '1' | '2' = '1' // 控制背景的淡入淡出
//前景层
@State imgUrl1: string | null = null
@State imgUrl2: string | null = null
@State foreUrl1: string = ''
@State foreUrl2: string = ''
@State animConfig1: PhotoAnimConfig | null = null//动画配置
@State animConfig2: PhotoAnimConfig | null = null
@State activeFore: '1' | '2' = '1' // 当前显示哪一个
private timer: number = -1
aboutToAppear() {
//走跳转方式的,从 TV 照片上墙页过来的 拿数据
const currentParam = HMRouterMgr.getCurrentParam()
const paramsMap = currentParam as Record<string, Object>
if (paramsMap && paramsMap['urls']) {
const items = paramsMap['urls'] as proto_kg_tv.photoUrlItem[]
const urls = items.map(item => item.fileUrl) // 提取 url 字符串
this.addPhotos(urls)
}
this.timer = setInterval(() => {
this.nextPhoto()
}, 2500)
}
aboutToDisappear() {
clearInterval(this.timer)
photoQueueManager.clear()
}
build() {
Stack() {
Image(this.imgBGUrl1)
.width('100%')
.height('100%')
.blur(20)
.opacity(this.activeBg === '1' ? 1 : 0) // 状态驱动
.animation({ duration: 1500 }) // 1.5秒淡入淡出
Image(this.imgBGUrl2)
.width('100%')
.height('100%')
.blur(20)
.opacity(this.activeBg === '2' ? 1 : 0)
.animation({ duration: 1500 })
// 前景 1
PhotoWayItem({
imgUrl: this.foreUrl1,
config: this.animConfig1,
isActive: this.activeFore === '1' // 传入激活状态
})
// 前景 2
PhotoWayItem({
imgUrl: this.foreUrl2,
config: this.animConfig2,
isActive: this.activeFore === '2'
})
}
.width('100%')
.height('100%')
}
// 添加照片数据
public addPhotos(urls: string[]) {
urls.forEach(url => photoQueueManager.addPhotoUrl(url))
// 只要都没有 URL,就说明是初始状态,立即开始
if (!this.foreUrl1 && !this.foreUrl2) {
this.nextPhoto()
}
}
nextPhoto() {
let newUrl = photoQueueManager.getNextUrl()
if (!newUrl) return
// 处理背景 (简单的淡入淡出)
if (this.activeBg === '1') {
this.imgBGUrl2 = newUrl
this.activeBg = '2' // 2 淡入,1 淡出
} else {
this.imgBGUrl1 = newUrl
this.activeBg = '1' // 1 淡入,2 淡出
}
// 处理前景 (复杂的漫游动画)
let newConfig = animHelper.getRandomAnimation()
if (this.activeFore === '1') {
// 准备 2 (加载新图,设置新动画)
this.foreUrl2 = newUrl
this.animConfig2 = newConfig
// 切换焦点到 2 (触发 2 淡入,1 淡出)
this.activeFore = '2'
} else {
// 准备 1
this.foreUrl1 = newUrl
this.animConfig1 = newConfig
// 切换焦点到 1
this.activeFore = '1'
}
}
}
动画管理类,用于管理动画形式
/**
* @文件名称: PhotoAnimHelper.ets
* @路径: entry/src/main/ets/playsongs/photosway
* @作者: 魔眼天王
* @创建时间: 2026/1/16 16:43
* @描述: 照片上墙动画工具类
*/
export class PhotoAnimHelper {
private lastRandom: number = 0; // 记录上一次的随机值,防止重复
// 默认初始状态
private static readonly DEFAULT_TRANSFORM: AnimTransform = {
scaleX: 1.0,
scaleY: 1.0,
rotateAngle: 0,
translateX: '0%',
translateY: '0%',
transformOriginX: '50%',
transformOriginY: '50%'
}
private static copyTransform(source: AnimTransform): AnimTransform {
return {
scaleX: source.scaleX,
scaleY: source.scaleY,
rotateAngle: source.rotateAngle,
translateX: source.translateX,
translateY: source.translateY,
transformOriginX: source.transformOriginX,
transformOriginY: source.transformOriginY
};
}
/**
* 生成随机动画配置
* @param isLongImage 是否是长图
*/
public getRandomAnimation(isLongImage: boolean = false): PhotoAnimConfig {
let r = Math.random();
// 防重复逻辑 如果随机数和上次太接近,或者长图遇到了纵向移动的随机段(0.3~0.4),则重随
let retryCount = 0;
while (retryCount < 5 && ((Math.abs(r - this.lastRandom) < 0.1) || (isLongImage && r <= 0.4 && r > 0.3))) {
r = Math.random();
retryCount++;
}
// 保留一位小数作为下次比较的基准
this.lastRandom = Math.floor(r * 10) / 10;
const duration = 2000; //动画时长
let fromState: AnimTransform = PhotoAnimHelper.copyTransform(PhotoAnimHelper.DEFAULT_TRANSFORM);
let toState: AnimTransform = PhotoAnimHelper.copyTransform(PhotoAnimHelper.DEFAULT_TRANSFORM);
if (r > 0.9) {
// 组合1: 右旋 + 放大
fromState.rotateAngle = 0;
toState.rotateAngle = 10;
fromState.scaleX = 1.0;
toState.scaleX = 1.05;
fromState.scaleY = 1.0;
toState.scaleY = 1.05;
} else if (r > 0.8) {
// 组合2: 左旋 + 缩小
fromState.rotateAngle = 0;
toState.rotateAngle = -10;
fromState.scaleX = 1.05;
toState.scaleX = 1.0;
fromState.scaleY = 1.05;
toState.scaleY = 1.0;
} else if (r > 0.7) {
// 组合3: 中心点偏上的放大 (Pivot Y=40%)
fromState.transformOriginY = '40%';
toState.transformOriginY = '40%';
fromState.scaleX = 1.0;
toState.scaleX = 1.05;
fromState.scaleY = 1.0;
toState.scaleY = 1.05;
} else if (r > 0.6) {
// 组合4: 放大 + 右移漫游 (X: -3% -> 3%)
fromState.scaleX = 1.0;
toState.scaleX = 1.05;
fromState.scaleY = 1.0;
toState.scaleY = 1.05;
fromState.translateX = '-3%';
toState.translateX = '3%';
} else if (r > 0.5) {
// 组合5: 中心点偏下的放大 (Pivot=60%)
fromState.transformOriginX = '60%';
toState.transformOriginX = '60%';
fromState.transformOriginY = '60%';
toState.transformOriginY = '60%';
fromState.scaleX = 1.0;
toState.scaleX = 1.05;
fromState.scaleY = 1.0;
toState.scaleY = 1.05;
} else if (r > 0.4) {
// 组合6: 放大 + 上移漫游 (Y: 10% -> -10%)
fromState.scaleX = 1.0;
toState.scaleX = 1.05;
fromState.scaleY = 1.0;
toState.scaleY = 1.05;
fromState.translateY = '10%';
toState.translateY = '-10%';
} else if (r > 0.3) {
// 组合7: 放大 + 下移漫游 (Y: -10% -> 10%)
fromState.scaleX = 1.0;
toState.scaleX = 1.05;
fromState.scaleY = 1.0;
toState.scaleY = 1.05;
fromState.translateY = '-10%';
toState.translateY = '10%';
} else if (r > 0.1) {
// 组合8 & 9 (0.1~0.3): 右旋 + 放大 (重复高频效果)
fromState.rotateAngle = 0;
toState.rotateAngle = 10;
fromState.scaleX = 1.0;
toState.scaleX = 1.05;
fromState.scaleY = 1.0;
toState.scaleY = 1.05;
} else {
// 兜底: 放大 + 右移
fromState.scaleX = 1.0;
toState.scaleX = 1.05;
fromState.scaleY = 1.0;
toState.scaleY = 1.05;
fromState.translateX = '-3%';
toState.translateX = '3%';
}
return {
duration: duration,
from: fromState,
to: toState
};
}
}
export const animHelper = new PhotoAnimHelper()
// 定义动画属性接口
export interface AnimTransform {
scaleX: number;
scaleY: number;
rotateAngle: number;
translateX: string; // 使用百分比字符串,如 '10%'
translateY: string;
transformOriginX: string; // 旋转/缩放中心点 X
transformOriginY: string; // 旋转/缩放中心点 Y
}
// 动画配置对象(包含开始和结束状态)
export interface PhotoAnimConfig {
duration: number; // 动画时长 (ms)
from: AnimTransform; // 起始状态
to: AnimTransform; // 结束状态
}
组件前景图的那个控件,写在一起代码太多了,就给摘出来了。
/**
* @文件名称: PhotoWayItem.ets
* @路径: entry/src/main/ets/playsongs/photosway
* @作者: 魔眼天王
* @创建时间: 2026/1/16 16:57
* @描述: 照片上墙模式下录唱页的那个带动画的图片展示控件
*/
@Component
export struct PhotoWayItem {
@Prop imgUrl: string
@Prop config: PhotoAnimConfig|null
@Prop @Watch('onActiveChange')isActive: boolean = false // 控制显隐和动画触发
@State currentTransform: AnimTransform = {
scaleX: 1, scaleY: 1, rotateAngle: 0,
translateX: '0%', translateY: '0%',
transformOriginX: '50%', transformOriginY: '50%'
}
aboutToAppear() {
// 首次加载如果就是激活状态,也要触发
if (this.isActive) {
this.onActiveChange()
}
}
build() {
Image(this.imgUrl)
.width('100%')
.height('100%')
.objectFit(ImageFit.Contain)
.alt($r('app.media.page_bg'))
// --- 绑定动画属性 ---
.scale({
x: this.currentTransform.scaleX,
y: this.currentTransform.scaleY,
centerX: this.currentTransform.transformOriginX,
centerY: this.currentTransform.transformOriginY
})
.rotate({
angle: this.currentTransform.rotateAngle,
centerX: this.currentTransform.transformOriginX,
centerY: this.currentTransform.transformOriginY
})
.translate({
x: this.currentTransform.translateX,
y: this.currentTransform.translateY
})
.opacity(this.isActive ? 1 : 0)
.animation({ duration: 2000, curve: Curve.Linear })
}
// 监听 isActive 变化
onActiveChange() {
if (this.isActive&& this.config) {
// === 进场逻辑 ===
// 立即重置到起始状态
this.currentTransform = this.config.from
// 执行漫游动画 (2秒)
animateTo({
duration: this.config?.duration,
curve: Curve.FastOutSlowIn,
playMode: PlayMode.Normal,
}, () => {
if (this.config) {
this.currentTransform = this.config.to
}
})
}
}
}
队列管理器,用于管理前后展示逻辑。
/**
* @文件名称: PhotoQueueManager.ets
* @路径: entry/src/main/ets/playsongs/photosway
* @作者: 魔眼天王
* @创建时间: 2026/1/16 16:40
* @描述: 照片墙队列管理器
*/
export class PhotoQueueManager {
private waitUrls: string[] = []; // 等待展示的队列 (urls)
private shownUrls: string[] = []; // 已经展示过的历史池 (oldUrls)
/**
* 添加新照片
* 逻辑:50%概率插队头,50%插队尾
*/
public addPhotoUrl(url: string): void {
if (!url) return;
// 简单防重 如果已经在等待队列里,就不加了
if (this.waitUrls.includes(url)) return;
if (Math.random() > 0.5) {
this.waitUrls.unshift(url); // 插队头
} else {
this.waitUrls.push(url); // 插队尾
}
}
/**
* 批量移除照片(比如用户撤回)
*/
public removeBatchUrls(urls: string[]): void {
if (!urls || urls.length === 0) return;
// 从两个队列里都删掉
this.waitUrls = this.waitUrls.filter(u => !urls.includes(u));
this.shownUrls = this.shownUrls.filter(u => !urls.includes(u));
}
/**
* 获取下一张要展示的照片
* @returns 图片url,如果没有图片则返回 null
*/
public getNextUrl(): string | null {
//如果等待队列空了,但历史池有货 -> 把历史池倒回来 (实现循环)
if (this.waitUrls.length === 0 && this.shownUrls.length > 0) {
this.waitUrls = [...this.shownUrls];
this.shownUrls = []; // 清空历史池
}
//还没货?那就是真没图了
if (this.waitUrls.length === 0) {
return null;
}
// 取出第一张
const url = this.waitUrls.shift();
// 放入历史池 以此实现无限循环
if (url) {
this.shownUrls.push(url);
}
return url || null;
}
/**
* 重置/清空
*/
public clear(): void {
this.waitUrls = [];
this.shownUrls = [];
}
/**
* 是否有图
*/
public hasPhotos(): boolean {
return this.waitUrls.length > 0 || this.shownUrls.length > 0;
}
}
export const photoQueueManager = new PhotoQueueManager
更多推荐
所有评论(0)