更新记录
1.0.1(2026-04-07)
- 支持 Android 原生图片编辑 UI
- 支持裁剪、旋转、翻转、滤镜、文字、贴纸、涂鸦等功能
- 内置3套贴纸素材
- 回调返回编辑后图片的绝对路径
1.0.0(2026-04-07)
初始版
平台兼容性
uni-app(4.31)
| Vue2 | Vue2插件版本 | Vue3 | Vue3插件版本 | Chrome | Safari | app-vue | app-vue插件版本 | app-nvue | app-nvue插件版本 | Android | Android插件版本 | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| √ | 1.0.0 | √ | 1.0.0 | - | - | √ | 1.0.0 | √ | 1.0.0 | 5.0 | 1.0.0 | - | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
uni-app x(4.31)
| Chrome | Safari | Android | Android插件版本 | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|---|
| - | - | 5.0 | 1.0.0 | × | × | × |
fz-image-editor 图片编辑器插件
基于 UTS 原生插件开发的图片编辑器,支持裁剪、旋转、滤镜、文字、贴纸、涂鸦等丰富功能,提供完整原生编辑 UI 界面。
功能特性
- 图片裁剪(自由/比例裁剪)
- 图片旋转与翻转
- 滤镜效果(多种风格)
- 文字添加
- 贴纸添加(内置多套贴纸素材)
- 手绘涂鸦
- 马赛克/模糊处理
- 亮度/对比度调节
- 一键撤销/重做
- 编辑完成后回调返回新图片路径
环境要求
- HBuilderX 4.0+
- uni-app或uni-app x 项目
- Android minSdkVersion:21(Android 5.0+)
安装方式
将 fz-image-editor 目录复制到项目的 uni_modules 目录下,目录结构如下:
Android 权限说明
插件已在 AndroidManifest.xml 中自动声明以下权限,无需手动配置:
| 权限 | 用途 |
|---|---|
WRITE_EXTERNAL_STORAGE |
保存编辑后的图片 |
READ_MEDIA_IMAGES |
读取图库图片(Android 11+) |
READ_MEDIA_VIDEO |
读取媒体文件(Android 11+) |
READ_MEDIA_VISUAL_USER_SELECTED |
用户选择媒体权限(Android 11+) |
CAMERA |
拍摄照片功能 |
API 说明
ImageEditorUI.openEditor(options, callback)
打开原生图片编辑 UI 界面。
参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| options | EditImageOptions |
是 | 编辑选项 |
| callback | (result: EditResult) => void |
是 | 编辑完成回调 |
EditImageOptions 类型:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| imagePath | string |
是 | 待编辑图片的绝对路径 |
| outputPath | string \| null |
否 | 编辑结果保存路径,不传则自动生成 |
EditResult 类型:
| 字段 | 类型 | 说明 |
|---|---|---|
| path | string |
编辑后图片的绝对路径,未编辑时为空字符串 |
| isEdited | boolean |
是否完成了编辑(true=已保存,false=取消编辑) |
快速开始
uvue 示例代码
<template>
<!-- #ifdef APP -->
<scroll-view style="flex: 1;">
<!-- #endif -->
<view class="container">
<!-- 选择图片区域 -->
<view v-if="imagePath == ''" class="upload-area" @click="chooseImage">
<text class="upload-icon">+</text>
<text class="upload-text">点击选择图片</text>
</view>
<!-- 图片预览区域 -->
<view v-if="imagePath != ''" class="preview-area">
<image class="preview-image" :src="imagePath" mode="aspectFit"></image>
</view>
<!-- 操作按钮 -->
<view class="btn-group">
<button v-if="imagePath != ''" class="btn-primary" @click="openEditor">
开始编辑
</button>
<button v-if="resultPath != ''" class="btn-success" @click="saveToAlbum">
保存到相册
</button>
</view>
<!-- 编辑结果展示 -->
<view v-if="resultPath != ''" class="result-area">
<text class="result-label">编辑完成</text>
<image class="result-image" :src="resultPath" mode="aspectFit"></image>
<text class="result-path">保存路径:{{ resultPath }}</text>
</view>
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</template>
<script lang="uts">
import { ImageEditorUI, EditImageOptions, EditResult } from '@/uni_modules/fz-image-editor'
export default {
data() {
return {
imagePath: '' as string,
resultPath: '' as string
}
},
methods: {
// 选择本地图片
chooseImage() {
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: (res) => {
// tempFilePaths 返回的是 file:// 开头的路径
// 需要去除 file:// 前缀,使用绝对路径
let tempPath = res.tempFilePaths[0]
if (tempPath.startsWith('file://')) {
tempPath = tempPath.replace('file://', '')
}
this.imagePath = tempPath
this.resultPath = ''
},
fail: (err) => {
console.log('选择图片失败:', err)
}
})
},
// 打开图片编辑器
openEditor() {
if (this.imagePath == '') {
uni.showToast({ title: '请先选择图片', icon: 'none' })
return
}
const options : EditImageOptions = {
imagePath: this.imagePath,
// outputPath 可选,不传时自动生成到缓存目录
// outputPath: uni.env.USER_DATA_PATH + '/edited_' + Date.now() + '.jpg'
}
ImageEditorUI.openEditor(options, (result : EditResult) => {
if (result.isEdited) {
console.log('编辑成功,路径:', result.path)
this.resultPath = result.path
this.imagePath = result.path
uni.showToast({ title: '编辑成功', icon: 'success' })
} else {
console.log('用户取消编辑')
uni.showToast({ title: '已取消编辑', icon: 'none' })
}
})
},
// 保存到相册
saveToAlbum() {
if (this.resultPath == '') return
uni.saveImageToPhotosAlbum({
filePath: this.resultPath,
success: () => {
uni.showToast({ title: '已保存到相册', icon: 'success' })
},
fail: (err) => {
uni.showToast({ title: '保存失败:' + err.errMsg, icon: 'none' })
}
})
}
}
}
</script>
<style>
.container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.upload-area {
width: 200px;
height: 200px;
border: 2px dashed #cccccc;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 40px;
}
.upload-icon {
font-size: 48px;
color: #999999;
line-height: 1;
}
.upload-text {
font-size: 14px;
color: #999999;
margin-top: 8px;
}
.preview-area {
width: 100%;
margin-top: 16px;
}
.preview-image {
width: 100%;
height: 300px;
border-radius: 8px;
}
.btn-group {
display: flex;
flex-direction: row;
margin-top: 20px;
gap: 12px;
}
.btn-primary {
background-color: #7B2FBE;
color: #ffffff;
border-radius: 24px;
font-size: 16px;
padding: 12px 32px;
border: none;
}
.btn-success {
background-color: #0e932e;
color: #ffffff;
border-radius: 24px;
font-size: 16px;
padding: 12px 32px;
border: none;
}
.result-area {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 24px;
width: 100%;
}
.result-label {
font-size: 16px;
font-weight: bold;
color: #0e932e;
}
.result-image {
width: 100%;
height: 300px;
border-radius: 8px;
margin-top: 8px;
}
.result-path {
font-size: 12px;
color: #666666;
margin-top: 8px;
word-break: break-all;
}
</style>
vue 示例代码
<template>
<!-- #ifdef APP -->
<scroll-view style="flex: 1;">
<!-- #endif -->
<view class="page-wrap">
<view v-if="imagePath == ''" class="picker-box" @click="pickImage">
<text class="picker-plus">+</text>
<text class="picker-hint">点击选择或拍摄图片</text>
</view>
<view v-if="imagePath != ''" class="img-box">
<image class="img-preview" :src="imagePath" mode="aspectFit" />
<text class="img-tip">点击下方按钮开始编辑</text>
</view>
<view class="action-row">
<view class="btn btn-purple" @click="pickImage">
<text class="btn-text">选择图片</text>
</view>
<view v-if="imagePath != ''" class="btn btn-blue" @click="startEdit">
<text class="btn-text">编辑图片</text>
</view>
</view>
<view v-if="resultPath != ''" class="result-card">
<text class="result-title">编辑结果</text>
<image class="result-img" :src="resultPath" mode="aspectFit" />
<view class="result-actions">
<view class="btn btn-green" @click="doSave">
<text class="btn-text">保存相册</text>
</view>
<view class="btn btn-orange" @click="doShare">
<text class="btn-text">分享图片</text>
</view>
</view>
</view>
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</template>
<script>
import { ImageEditorUI } from '@/uni_modules/fz-image-editor'
export default {
data() {
return {
imagePath: '',
resultPath: ''
}
},
methods: {
pickImage() {
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: (res) => {
let p = res.tempFilePaths[0]
// 去除 file:// 协议前缀,保留绝对路径
if (p.startsWith('file://')) {
p = p.replace('file://', '')
}
this.imagePath = p
this.resultPath = ''
}
})
},
startEdit() {
if (this.imagePath == '') {
uni.showToast({ title: '请先选择图片', icon: 'none' })
return
}
ImageEditorUI.openEditor(
{ imagePath: this.imagePath },
(result) => {
if (result.isEdited) {
this.resultPath = result.path
this.imagePath = result.path
uni.showToast({ title: '编辑完成', icon: 'success' })
} else {
uni.showToast({ title: '已取消', icon: 'none' })
}
}
)
},
doSave() {
uni.saveImageToPhotosAlbum({
filePath: this.resultPath,
success: () => uni.showToast({ title: '保存成功', icon: 'success' }),
fail: () => uni.showToast({ title: '保存失败', icon: 'error' })
})
},
doShare() {
uni.shareWithSystem({
imageUrl: this.resultPath,
type: 'image',
success(res) { console.log('分享成功', res) },
fail(err) { console.log('分享失败', err) }
})
}
}
}
</script>
<style>
.page-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 16px;
}
.picker-box {
width: 180px;
height: 180px;
border: 2px dashed #bbbbbb;
border-radius: 16px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 30px;
}
.picker-plus {
font-size: 52px;
color: #aaaaaa;
line-height: 1;
}
.picker-hint {
font-size: 13px;
color: #aaaaaa;
margin-top: 6px;
}
.img-box {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 16px;
}
.img-preview {
width: 100%;
height: 260px;
border-radius: 10px;
}
.img-tip {
font-size: 13px;
color: #888888;
margin-top: 8px;
}
.action-row {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 20px;
gap: 16px;
}
.btn {
padding: 12px 28px;
border-radius: 28px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.btn-text {
color: #ffffff;
font-size: 15px;
}
.btn-purple {
background-color: #7B2FBE;
}
.btn-blue {
background-color: #1677ff;
}
.btn-green {
background-color: #0e932e;
}
.btn-orange {
background-color: #fa8c16;
}
.result-card {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 28px;
width: 100%;
background-color: #f6fff8;
border-radius: 12px;
padding: 16px;
border: 1px solid #d9f7be;
}
.result-title {
font-size: 16px;
font-weight: bold;
color: #0e932e;
}
.result-img {
width: 100%;
height: 260px;
border-radius: 8px;
margin-top: 12px;
}
.result-actions {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 14px;
gap: 16px;
}
</style>
进阶使用
自定义输出路径
import { ImageEditorUI, EditImageOptions, EditResult } from '@/uni_modules/fz-image-editor'
// 自定义输出路径(需确保目录存在)
const outputDir = uni.env.USER_DATA_PATH + '/my_edits/'
const outputPath = outputDir + 'result_' + Date.now() + '.jpg'
ImageEditorUI.openEditor(
{
imagePath: '/sdcard/DCIM/test.jpg',
outputPath: outputPath
},
(result : EditResult) => {
if (result.isEdited) {
console.log('保存到自定义路径:', result.path)
}
}
)
结合 uni.chooseImage 完整流程
import { ImageEditorUI } from '@/uni_modules/fz-image-editor'
// 第一步:选择图片
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: (res) => {
// 第二步:去除 file:// 前缀获取绝对路径
let absPath = res.tempFilePaths[0].replace('file://', '')
// 第三步:打开编辑器
ImageEditorUI.openEditor({ imagePath: absPath }, (result) => {
if (result.isEdited) {
// 第四步:使用编辑后的图片路径(例如上传到服务器)
console.log('编辑后路径:', result.path)
uploadImage(result.path)
}
})
}
})
function uploadImage(filePath : string) {
uni.uploadFile({
url: 'https://your-server.com/upload',
filePath: filePath,
name: 'file',
success: (uploadRes) => {
console.log('上传成功', uploadRes.data)
}
})
}
常见问题
Q1:imagePath 传入后编辑器未打开或提示路径错误?
原因: uni.chooseImage 返回的 tempFilePaths 是 file:// 协议路径,需去掉前缀。
解决方案:
let path = res.tempFilePaths[0]
if (path.startsWith('file://')) {
path = path.replace('file://', '')
}
Q2:用户点击编辑器返回按钮,callback 中 isEdited 为 false?
这是正常行为。当用户点击编辑器的返回/取消按钮时,插件不保存文件,回调中 isEdited = false,path 为空字符串。
Q3:编辑完成后图片没有变化?
请检查 result.isEdited 是否为 true,并使用 result.path 更新显示的图片路径,而非原始路径。
Q4:outputPath 自定义路径写入失败?
请确保:
- 目录已存在,或使用系统缓存目录(插件默认行为)。
- 路径不以
file://开头,使用纯绝对路径。
Q5:打包后运行报找不到 Activity?
确认插件已正确放置在 uni_modules/fz-image-editor 下,且 AndroidManifest.xml 中的 Activity 声明已被合并到最终包中。重新进行自定义基座编译即可。
更新日志
v1.0.0
- 初始版本发布
- 支持 Android 原生图片编辑 UI
- 支持裁剪、旋转、翻转、滤镜、文字、贴纸、涂鸦等功能
- 内置3套共60+贴纸素材
- 回调返回编辑后图片的绝对路径
License
MIT

收藏人数:
购买普通授权版(
试用
赞赏(0)
下载 268
赞赏 0
下载 11475088
赞赏 1902
赞赏
京公网安备:11010802035340号