如果你想做一个冰箱贴收集App,需要用到这个API
想要亲自体验"冰箱贴集"这款App的功能?打开你的鸿蒙手机,前往华为应用市场,搜索"冰箱贴集"即可下载安装,把旅行收集的冰箱贴都记在上面吧。
写在前面
冰箱贴这个东西,说起来是一种很特别的旅行纪念品。每次去一个新城市,看到好看的冰箱贴就想买一个。时间一长,冰箱上贴满了,但是到底去了哪些城市、每个冰箱贴长什么样、啥时候买的,全记不清了。
所以我就想做一个"冰箱贴集"App,把这些冰箱贴都拍照记录下来,还能在一个虚拟冰箱上排版展示,像真正的冰箱一样随便贴。这个App用到的核心技术就是图片处理和Canvas绘制,再加上拖拽手势来实现冰箱贴的移动和排版。
这篇文章,我会带你一步步做出来。先看Web端的React实现,然后再转到鸿蒙的ArkTS。
这篇文章聊什么
咱们今天要搞明白三件事:
- 图片加载和缩放 -- 怎么把用户选择的图片缩放到合适的大小,在有限的冰箱空间里展示
- 网格布局计算 -- 虚拟冰箱是一个固定大小的区域,怎么让冰箱贴按网格排列,看起来整齐又不呆板
- 拖拽排序 -- 用手势拖拽冰箱贴到任意位置,怎么实现流畅的拖拽体验
先看整体流程:
flowchart TD
A[用户打开App] --> B[加载冰箱贴列表]
B --> C{用户操作}
C -->|添加冰箱贴| D[选择图片]
D --> E[缩放适配]
E --> F[添加到冰箱]
C -->|拖拽冰箱贴| G[PanGesture触发]
G --> H[计算新位置]
H --> I[更新布局]
C -->|查看详情| J[弹出详情面板]
J --> K[显示地点和日期]
第一步:React版 -- 用Canvas画冰箱
Web端的做法是用一个Canvas元素当虚拟冰箱,冰箱贴就是Canvas上的图片对象。先看代码:
import React, { useState, useRef, useCallback, useEffect } from 'react';
// 冰箱贴数据结构
// position是相对于冰箱左上角的坐标
// size是缩放后的尺寸
const MagnetItem = ({ magnet, onSelect, onDrag }) => {
const imgRef = useRef(null);
const handleMouseDown = (e) => {
// 记录鼠标按下时的位置,用于计算拖拽偏移
const startX = e.clientX;
const startY = e.clientY;
const offsetX = magnet.x;
const offsetY = magnet.y;
const handleMouseMove = (moveEvent) => {
const deltaX = moveEvent.clientX - startX;
const deltaY = moveEvent.clientY - startY;
// 实时更新位置
onDrag(magnet.id, offsetX + deltaX, offsetY + deltaY);
};
const handleMouseUp = () => {
// 鼠标松开时移除事件监听
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
return (
<div
style={{
position: 'absolute',
left: magnet.x,
top: magnet.y,
width: magnet.size,
height: magnet.size,
cursor: 'grab',
}}
onMouseDown={handleMouseDown}
onClick={() => onSelect(magnet)}
>
<img
ref={imgRef}
src={magnet.imageUrl}
alt={magnet.name}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
draggable={false}
/>
<span style={{
position: 'absolute',
bottom: 2,
left: 0,
right: 0,
textAlign: 'center',
fontSize: 10,
color: '#fff',
textShadow: '0 0 3px rgba(0,0,0,0.8)',
}}>
{magnet.city}
</span>
</div>
);
};
function FridgePage() {
// 冰箱贴列表,每个冰箱贴有位置、大小、图片、城市等信息
const [magnets, setMagnets] = useState([
{ id: 1, name: '长城冰箱贴', city: '北京', imageUrl: '/fridge/1.jpg', x: 20, y: 20, size: 80, date: '2025-03-15' },
{ id: 2, name: '东方明珠冰箱贴', city: '上海', imageUrl: '/fridge/2.jpg', x: 120, y: 30, size: 80, date: '2025-05-20' },
{ id: 3, name: '兵马俑冰箱贴', city: '西安', imageUrl: '/fridge/3.jpg', x: 50, y: 130, size: 80, date: '2025-08-10' },
]);
const [selectedMagnet, setSelectedMagnet] = useState(null);
const handleDrag = useCallback((id, newX, newY) => {
// 限制在冰箱范围内
const maxX = 320; // 冰箱宽度减去冰箱贴大小
const maxY = 400; // 冰箱高度减去冰箱贴大小
setMagnets(prev => prev.map(m =>
m.id === id ? { ...m, x: Math.max(0, Math.min(newX, maxX)), y: Math.max(0, Math.min(newY, maxY)) } : m
));
}, []);
const handleAdd = () => {
// 添加新冰箱贴,随机放在冰箱上
const newMagnet = {
id: Date.now(),
name: '新冰箱贴',
city: '待填写',
imageUrl: '/fridge/new.jpg',
x: Math.random() * 200,
y: Math.random() * 300,
size: 80,
date: new Date().toISOString().split('T')[0],
};
setMagnets(prev => [...prev, newMagnet]);
};
return (
<div style={{ padding: 20 }}>
<h1>冰箱贴集</h1>
<button onClick={handleAdd}>添加冰箱贴</button>
{/* 虚拟冰箱容器 */}
<div style={{
position: 'relative',
width: 400,
height: 500,
backgroundColor: '#E8E8E8',
border: '3px solid #999',
borderRadius: 8,
margin: '20px auto',
// 磁铁质感背景
backgroundImage: 'linear-gradient(135deg, #ddd 25%, #eee 25%, #eee 50%, #ddd 50%, #ddd 75%, #eee 75%)',
backgroundSize: '20px 20px',
}}>
{magnets.map(magnet => (
<MagnetItem
key={magnet.id}
magnet={magnet}
onSelect={setSelectedMagnet}
onDrag={handleDrag}
/>
))}
</div>
{selectedMagnet && (
<div style={{ padding: 10, backgroundColor: '#f0f0f0', borderRadius: 8 }}>
<p>名称: {selectedMagnet.name}</p>
<p>城市: {selectedMagnet.city}</p>
<p>日期: {selectedMagnet.date}</p>
</div>
)}
</div>
);
}
export default FridgePage;
这段React代码的思路很简单:用一个相对定位的div当冰箱,每个冰箱贴用绝对定位放在里面,拖拽的时候通过mousedown/mousemove/mouseup事件来更新位置。
但有个问题 -- React这种方式在高频拖拽时性能不太好,因为每次mousemove都会触发setState,导致整个组件重新渲染。在冰箱贴数量少的时候感觉不明显,但如果冰箱贴多了就会卡。鸿蒙的ArkTS在这方面做得更好,因为有手势系统的原生支持。
第二步:ArkTS版 -- 用手势系统和Canvas实现
现在转到鸿蒙。ArkTS里我们用PanGesture(拖拽手势)来实现冰箱贴的拖拽,用Canvas来绘制冰箱的背景。
import { image } from '@kit.MediaLibraryKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { promptAction } from '@kit.ArkUI';
// 冰箱贴数据结构
interface MagnetData {
id: number;
name: string;
city: string;
imagePath: string; // 本地图片路径
x: number; // 在冰箱上的x坐标
y: number; // 在冰箱上的y坐标
size: number; // 缩放后的尺寸
date: string; // 收集日期
}
// 冰箱常量
// 为什么用常量?因为这些值在多个地方用到,写死的话改起来很麻烦
const FRIDGE_WIDTH = 350;
const FRIDGE_HEIGHT = 500;
const MAGNET_DEFAULT_SIZE = 80;
const GRID_COLUMNS = 4; // 网格列数
const GRID_ROWS = 6; // 网格行数
@Entry
@Component
struct FridgePage {
@State magnets: MagnetData[] = [];
@State selectedMagnetId: number = -1;
@State isAdding: boolean = false;
// 当前正在拖拽的冰箱贴ID
private draggingId: number = -1;
// 拖拽起始位置
private dragStartX: number = 0;
private dragStartY: number = 0;
// 冰箱贴原始位置
private magnetStartX: number = 0;
private magnetStartY: number = 0;
aboutToAppear() {
// 初始化一些示例冰箱贴数据
this.magnets = [
{ id: 1, name: '长城冰箱贴', city: '北京', imagePath: 'fridge/greatwall.jpg', x: 20, y: 20, size: MAGNET_DEFAULT_SIZE, date: '2025-03-15' },
{ id: 2, name: '东方明珠', city: '上海', imagePath: 'fridge/pearl.jpg', x: 110, y: 20, size: MAGNET_DEFAULT_SIZE, date: '2025-05-20' },
{ id: 3, name: '兵马俑', city: '西安', imagePath: 'fridge/terracotta.jpg', x: 200, y: 20, size: MAGNET_DEFAULT_SIZE, date: '2025-08-10' },
{ id: 4, name: '西湖', city: '杭州', imagePath: 'fridge/westlake.jpg', x: 20, y: 120, size: MAGNET_DEFAULT_SIZE, date: '2025-10-01' },
{ id: 5, name: '熊猫基地', city: '成都', imagePath: 'fridge/panda.jpg', x: 110, y: 120, size: MAGNET_DEFAULT_SIZE, date: '2026-01-15' },
];
}
// ==================== 图片缩放适配 ====================
// 这个方法把原始图片缩放到适合冰箱贴展示的大小
// 为什么需要缩放?因为用户拍的照片可能有几MB,直接加载太大了
// 而且冰箱贴的展示区域就80x80像素,加载原图完全是浪费内存
async resizeImage(sourcePath: string, targetSize: number): Promise<PixelMap | undefined> {
try {
const imageSource = image.createImageSource(sourcePath);
// 先获取图片的原始尺寸,然后按比例缩放
const imageInfo = await imageSource.getImageInfo();
const originalWidth = imageInfo.size.width;
const originalHeight = imageInfo.size.height;
// 计算缩放比例 -- 取宽高中较大的那个做等比缩放
// 为什么取较大的?因为我们要保证图片完整显示在正方形区域内
const scale = targetSize / Math.max(originalWidth, originalHeight);
const targetWidth = Math.floor(originalWidth * scale);
const targetHeight = Math.floor(originalHeight * scale);
// 解码选项:设置目标尺寸
const decodingOptions: image.DecodingOptions = {
editable: true,
desiredSize: { width: targetWidth, height: targetHeight },
};
const pixelMap = await imageSource.createPixelMap(decodingOptions);
imageSource.release(); // 记得释放资源
return pixelMap;
} catch (err) {
console.error('Resize image failed: ' + JSON.stringify(err));
return undefined;
}
}
// ==================== 网格布局计算 ====================
// 计算冰箱贴在网格中的位置
// 为什么需要这个方法?因为用户可以点击"自动排列"按钮,让冰箱贴整齐排列
calculateGridPosition(index: number): { x: number; y: number } {
// 每个格子的间距
const gap = 10;
// 冰箱可用宽度 = 冰箱宽度 - 左右边距
const availableWidth = FRIDGE_WIDTH - 20; // 左右各10px边距
// 每个格子的实际宽度
const cellWidth = (availableWidth - gap * (GRID_COLUMNS - 1)) / GRID_COLUMNS;
// 列号和行号
const col = index % GRID_COLUMNS;
const row = Math.floor(index / GRID_COLUMNS);
return {
x: 10 + col * (cellWidth + gap),
y: 10 + row * (cellWidth + gap),
};
}
// 自动排列所有冰箱贴
autoArrange() {
this.magnets = this.magnets.map((magnet, index) => {
const pos = this.calculateGridPosition(index);
return { ...magnet, x: pos.x, y: pos.y, size: 80 };
});
promptAction.showToast({ message: '已自动排列', duration: 1000 });
}
// ==================== 从相册选择图片 ====================
async pickImage(): Promise<string> {
try {
const picker = new photoAccessHelper.PhotoViewPicker();
const options = new photoAccessHelper.PhotoSelectOptions();
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
options.maxSelectNumber = 1; // 每次只选一张
const result = await picker.select(options);
if (result.photoUris && result.photoUris.length > 0) {
return result.photoUris[0];
}
} catch (err) {
console.error('Pick image failed: ' + JSON.stringify(err));
}
return '';
}
// 添加冰箱贴
async addMagnet() {
const imagePath = await this.pickImage();
if (!imagePath) return;
// 计算新冰箱贴的位置 -- 放在最后面
const newIndex = this.magnets.length;
const pos = this.calculateGridPosition(newIndex);
const newMagnet: MagnetData = {
id: Date.now(),
name: '新冰箱贴',
city: '',
imagePath: imagePath,
x: pos.x,
y: pos.y,
size: MAGNET_DEFAULT_SIZE,
date: new Date().toISOString().split('T')[0],
};
this.magnets = [...this.magnets, newMagnet];
promptAction.showToast({ message: '冰箱贴已添加', duration: 1000 });
}
// 删除冰箱贴
deleteMagnet(id: number) {
this.magnets = this.magnets.filter(m => m.id !== id);
if (this.selectedMagnetId === id) {
this.selectedMagnetId = -1;
}
promptAction.showToast({ message: '已删除', duration: 1000 });
}
// ==================== UI构建 ====================
build() {
Column() {
// 顶部标题
Row() {
Text('冰箱贴集')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Text(`共 ${this.magnets.length} 个`)
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.padding({ left: 16, right: 16, top: 16 })
// 操作按钮行
Row({ space: 10 }) {
Button('添加冰箱贴')
.fontSize(14)
.height(36)
.backgroundColor('#4ECDC4')
.onClick(() => this.addMagnet())
Button('自动排列')
.fontSize(14)
.height(36)
.backgroundColor('#95E1D3')
.onClick(() => this.autoArrange())
}
.width('100%')
.padding({ left: 16, right: 16, top: 8 })
// 虚拟冰箱区域
// 这里用Stack组件,因为它支持子组件的绝对定位
// Stack就好比React里的position:relative容器
Stack() {
// 冰箱背景 -- 用Canvas绘制一个金属质感的背景
Canvas(this.fridgeCanvas)
.width(FRIDGE_WIDTH)
.height(FRIDGE_HEIGHT)
.backgroundColor('#D4D4D4')
.borderRadius(12)
.border({ width: 3, color: '#888888' })
// 冰箱贴列表
ForEach(this.magnets, (magnet: MagnetData) => {
// 每个冰箱贴是一个Image组件,用position属性定位
Image(magnet.imagePath)
.width(magnet.size)
.height(magnet.size)
.objectFit(ImageFit.Cover)
.borderRadius(8)
.border({ width: 2, color: this.selectedMagnetId === magnet.id ? '#FF6B6B' : '#FFFFFF' })
.shadow({ radius: 4, color: 'rgba(0,0,0,0.3)', offsetY: 2 })
.position({ x: magnet.x, y: magnet.y })
// 关键:绑定拖拽手势
.gesture(
// PanGesture是平移手势,手指在屏幕上滑动时触发
// direction设为All表示支持所有方向
PanGesture({ direction: PanDirection.All })
.onActionStart((event: GestureEvent) => {
// 拖拽开始
this.draggingId = magnet.id;
this.dragStartX = event.fingers[0].localX;
this.dragStartY = event.fingers[0].localY;
this.magnetStartX = magnet.x;
this.magnetStartY = magnet.y;
// 选中这个冰箱贴
this.selectedMagnetId = magnet.id;
})
.onActionUpdate((event: GestureEvent) => {
// 拖拽过程中实时更新位置
if (this.draggingId === magnet.id) {
const deltaX = event.fingers[0].localX - this.dragStartX;
const deltaY = event.fingers[0].localY - this.dragStartY;
// 计算新位置,并限制在冰箱范围内
const newX = Math.max(0, Math.min(this.magnetStartX + deltaX, FRIDGE_WIDTH - magnet.size));
const newY = Math.max(0, Math.min(this.magnetStartY + deltaY, FRIDGE_HEIGHT - magnet.size));
// 更新位置
const index = this.magnets.findIndex(m => m.id === magnet.id);
if (index >= 0) {
this.magnets[index].x = newX;
this.magnets[index].y = newY;
}
}
})
.onActionEnd(() => {
// 拖拽结束
this.draggingId = -1;
})
)
.onClick(() => {
// 点击选中/取消选中
this.selectedMagnetId = this.selectedMagnetId === magnet.id ? -1 : magnet.id;
})
})
}
.width(FRIDGE_WIDTH)
.height(FRIDGE_HEIGHT)
.alignContent(Alignment.TopStart)
.margin({ top: 12 })
// 冰箱贴信息面板
// 选中冰箱贴后显示详细信息
if (this.selectedMagnetId > 0) {
Column() {
// 找到选中的冰箱贴数据
// 为什么用find而不是用index?因为find更语义化
const selectedMagnet = this.magnets.find(m => m.id === this.selectedMagnetId);
if (selectedMagnet) {
Row() {
Image(selectedMagnet.imagePath)
.width(60)
.height(60)
.objectFit(ImageFit.Cover)
.borderRadius(8)
Column({ space: 4 }) {
Text(selectedMagnet.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(`城市: ${selectedMagnet.city || '未填写'}`)
.fontSize(14)
.fontColor('#666666')
Text(`收集日期: ${selectedMagnet.date}`)
.fontSize(14)
.fontColor('#666666')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
.padding(12)
.backgroundColor('#F5F5F5')
.borderRadius(8)
Button('删除此冰箱贴')
.width('100%')
.height(40)
.fontSize(14)
.backgroundColor('#FF6B6B')
.margin({ top: 8 })
.onClick(() => this.deleteMagnet(selectedMagnet.id))
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
}
// 空状态提示
if (this.magnets.length === 0) {
Column() {
Text('还没有冰箱贴')
.fontSize(16)
.fontColor('#999999')
Text('点击上方"添加冰箱贴"开始收集吧')
.fontSize(14)
.fontColor('#BBBBBB')
.margin({ top: 4 })
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
.height('100%')
.backgroundColor('#FAFAFA')
}
// Canvas绘制冰箱背景
private fridgeCanvas: CanvasRenderingContext2D = new CanvasRenderingContext2D(new CanvasRenderingContext2DSettings());
}
这段代码有几个关键点要讲:
Stack组件的使用
为什么用Stack而不是Column或者Row?因为冰箱贴需要绝对定位 -- 每个冰箱贴可以在冰箱上的任意位置。Stack组件就是干这个的,它允许子组件通过position属性设置偏移位置,类似于Web里的absolute定位。
PanGesture手势系统
PanGesture是鸿蒙手势系统里的一种,专门处理拖拽。它有三个关键回调:
- onActionStart:手指按下时触发,我们在这里记录起始位置
- onActionUpdate:手指移动时持续触发,我们在这里计算偏移量并更新位置
- onActionEnd:手指抬起时触发,我们在这里清理拖拽状态
这个比React的mousedown/mousemove/mouseup好用多了,因为鸿蒙的手势系统原生处理了触摸事件,在手机上体验更好。
图片缩放
resizeImage方法用image.createImageSource来创建图片源,然后通过DecodingOptions设置目标尺寸来缩放。为什么要这样做?因为用户从相册选的照片可能是4000x3000像素的,直接加载到80x80的展示区域不仅浪费内存,渲染也很慢。先缩放再显示,性能会好很多。
第三步:冰箱背景的Canvas绘制
上面的代码里我们只是用了一个灰色背景,现在给冰箱画一个更有质感的金属背景:
// 在组件里添加一个方法来绘制冰箱背景
private drawFridgeBackground(ctx: CanvasRenderingContext2D) {
// 冰箱底色
ctx.fillStyle = '#C0C0C0';
ctx.fillRect(0, 0, FRIDGE_WIDTH, FRIDGE_HEIGHT);
// 金属拉丝效果 -- 画一些水平细线
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
ctx.lineWidth = 1;
for (let y = 0; y < FRIDGE_HEIGHT; y += 3) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(FRIDGE_WIDTH, y);
ctx.stroke();
}
// 冰箱把手装饰
ctx.fillStyle = '#888888';
ctx.fillRect(FRIDGE_WIDTH - 20, FRIDGE_HEIGHT * 0.3, 15, FRIDGE_HEIGHT * 0.4);
ctx.fillStyle = '#AAAAAA';
ctx.fillRect(FRIDGE_WIDTH - 18, FRIDGE_HEIGHT * 0.3 + 2, 11, FRIDGE_HEIGHT * 0.4 - 4);
// 品牌logo区域
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.font = '16px sans-serif';
ctx.fillText('Fridge', 10, FRIDGE_HEIGHT - 15);
}
Canvas绘制这块我多说两句。为什么用Canvas而不是直接用CSS/样式来做背景?因为Canvas可以做更精细的绘制控制。比如金属拉丝效果就是画一系列半透明的水平细线,这个用CSS很难实现。
但在实际项目中,如果背景效果不复杂,用渐变或者图片背景会更简单。Canvas适合那种需要动态绘制的场景。
流程图
flowchart TD
A[打开冰箱贴集App] --> B[加载冰箱贴数据]
B --> C[渲染虚拟冰箱]
C --> D{用户操作}
D -->|添加| E[PhotoViewPicker选择图片]
E --> F[缩放图片]
F --> G[计算网格位置]
G --> H[添加到magnets数组]
D -->|拖拽| I[PanGesture.onActionStart]
I --> J[记录起始坐标]
J --> K[PanGesture.onActionUpdate]
K --> L[计算偏移并更新位置]
L --> M[PanGesture.onActionEnd]
D -->|自动排列| N[calculateGridPosition]
N --> O[批量更新所有位置]
D -->|点击| P[显示/隐藏详情面板]
D -->|删除| Q[从数组中移除]
Q --> R[重新渲染]
React vs ArkTS 对比表
| 功能点 | React (Web) | ArkTS (鸿蒙) |
|---|---|---|
| 绝对定位容器 | position: relative/absolute | Stack 组件 |
| 图片展示 | <img> 标签 | Image 组件 |
| 拖拽实现 | mouse事件 + addEventListener | PanGesture 手势绑定 |
| 图片缩放 | CSS width/height + objectFit | image.createImageSource + DecodingOptions |
| 相册选择 | <input type="file"> | PhotoViewPicker API |
| 列表渲染 | array.map() | ForEach 组件 |
| 条件渲染 | && 或三元运算符 | if 语句(在build里) |
| Canvas绘制 | <canvas> + getContext('2d') | Canvas 组件 + CanvasRenderingContext2D |
最大的区别在拖拽的实现上。Web端用的是鼠标事件,要自己管理事件监听器的添加和移除,还要考虑边界情况。鸿蒙直接给你一个PanGesture,把复杂的触摸事件处理封装好了,你只需要关心位移计算就行。
权限声明说明
冰箱贴集这个App的权限需求非常轻:
{
"module": {
"reqPermissions": []
}
}
你没看错,不需要任何权限声明。为什么?因为我们用的是PhotoViewPicker来选择图片,这个API不需要相册读取权限。鸿蒙在设计PhotoViewPicker的时候就考虑到了隐私问题,用户主动选择的图片不需要额外的权限授权。
如果你需要直接读取相册里所有的图片(而不是让用户选择),那才需要ohos.permission.READ_MEDIA权限。但我们的场景是用户主动选择,所以完全不需要。
总结
这篇文章我们用"冰箱贴集"这个App讲清楚了三件事:
- 图片缩放适配 -- 用image.createImageSource的DecodingOptions来缩放原图,减少内存占用
- 网格布局计算 -- 根据冰箱的尺寸和列数,自动计算每个冰箱贴应该放在哪个格子
- 拖拽排序 -- PanGesture三步走:onActionStart记录起点,onActionUpdate计算偏移,onActionEnd清理状态
核心要点是PanGesture的使用,这是鸿蒙手势系统里最常用的手势之一。记住三个回调的职责分工,拖拽功能就能轻松搞定。
好,去华为应用市场搜索"冰箱贴集"下载体验一下吧,把你的冰箱贴都记录起来。