本文档记录了在基础项目完成后,进一步实现的进阶功能。进阶任务包括工程化插件开发(P0)和图像编辑功能实现(P2)。
编写一个自定义 Gradle 插件,为项目增加自动化构建能力:在 App 编译前清除指定目录下的缓存。
在 app/build.gradle.kts 中定义了一个自定义 Task cleanCacheDir,并将其配置为 preBuild 任务的依赖,确保每次编译前自动执行缓存清理。
// 定义清除缓存的 Task
tasks.register("cleanCacheDir") {
group = "cache"
description = "Clears specific build cache directories."
doLast {
// 指定要清除的缓存目录
val cacheDirs = listOf(
file("${project.buildDir}/intermediates/transforms"),
file("${project.buildDir}/tmp"),
// 可以添加更多需要清除的目录
)
cacheDirs.forEach { dir ->
if (dir.exists()) {
println("Clearing cache directory: ${dir.absolutePath}")
dir.deleteRecursively()
println("Cache directory cleared: ${dir.absolutePath}")
} else {
println("Cache directory does not exist, skipping: ${dir.absolutePath}")
}
}
}
}
// 让 preBuild 任务依赖于 cleanCacheDir,确保编译前执行清除
tasks.named("preBuild") {
dependsOn("cleanCacheDir")
}- Task 注册:使用
tasks.register()方法注册一个名为cleanCacheDir的自定义任务 - 任务分组:将任务归类到
cache组,便于在 Gradle 任务列表中查找 - 缓存目录配置:定义需要清理的缓存目录列表,包括:
build/intermediates/transforms:中间转换文件build/tmp:临时文件
- 安全删除:在删除前检查目录是否存在,避免不必要的错误
- 任务依赖:通过
dependsOn()将cleanCacheDir设置为preBuild的依赖,确保编译前自动执行
由于已配置为 preBuild 的依赖,每次执行编译命令时会自动执行:
./gradlew assembleDebug
# 或
./gradlew build./gradlew cleanCacheDir执行编译时,控制台输出示例:
> Task :app:cleanCacheDir
Clearing cache directory: D:\APP\MyApplication\app\build\intermediates\transforms
Cache directory cleared: D:\APP\MyApplication\app\build\intermediates\transforms
Cache directory does not exist, skipping: D:\APP\MyApplication\app\build\tmp
> Task :app:preBuild
> Task :app:generateDebugBuildConfig
...
该 Task 可以根据需要扩展,例如:
// 扩展:在编译完成后打印依赖树并写入文件
tasks.register("printDependencyTree") {
group = "reporting"
description = "Prints project dependency tree to a file."
doLast {
val outputFile = file("${project.buildDir}/reports/dependency-tree.txt")
outputFile.parentFile.mkdirs()
val dependencyTree = StringBuilder()
dependencyTree.appendLine("=== Project Dependencies ===")
dependencyTree.appendLine("Generated at: ${java.time.LocalDateTime.now()}")
dependencyTree.appendLine()
configurations.filter { it.isCanBeResolved }.forEach { config ->
try {
dependencyTree.appendLine("Configuration: ${config.name}")
config.resolvedConfiguration.firstLevelModuleDependencies.forEach { dep ->
dependencyTree.appendLine(" - ${dep.moduleGroup}:${dep.moduleName}:${dep.moduleVersion}")
}
dependencyTree.appendLine()
} catch (e: Exception) {
// 忽略无法解析的配置
}
}
outputFile.writeText(dependencyTree.toString())
println("Dependency tree written to: ${outputFile.absolutePath}")
}
}实现了六种核心编辑操作:
| 功能 | 描述 | 实现状态 |
|---|---|---|
| 手势操作 | 支持双指缩放和单指拖拽平移画布 | ✅ 已实现 |
| 比例裁剪 | 提供多种固定裁剪比例,允许用户调整裁剪框 | ✅ 已实现 |
| Undo/Redo | 支持操作的撤销与恢复 | ✅ 已实现 |
| 滤镜效果 | 提供多种预设滤镜,基于 OpenGL ES 着色器实现 | ✅ 已实现 |
| 图片旋转 | 支持 90° 增量旋转,通过纹理坐标变换实现 | ✅ 已实现 |
| 参数调整 | 支持亮度、对比度、饱和度等 9 种参数实时调整 | ✅ 已实现 |
手势操作通过 Android 的 ScaleGestureDetector 和 GestureDetector 实现,支持:
- 双指缩放:以双指中心点为焦点进行缩放,缩放范围限制在 0.1x ~ 5.0x
- 单指拖拽:平移画布,支持边界约束
- 惯性滑动:快速滑动后的惯性动画效果
2.2.2 核心代码 (EditorActivity.kt)
class EditorActivity : AppCompatActivity() {
private lateinit var scaleGestureDetector: ScaleGestureDetector
private lateinit var gestureDetector: GestureDetector
// 手势相关变量
private var lastTouchX: Float = 0.0f
private var lastTouchY: Float = 0.0f
private var activePointerId: Int = MotionEvent.INVALID_POINTER_ID
private var isScaling: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
scaleGestureDetector = ScaleGestureDetector(this, ScaleListener())
gestureDetector = GestureDetector(this, GestureListener())
}
override fun onTouchEvent(event: MotionEvent): Boolean {
// 裁剪模式或滤镜模式下不处理图片的缩放和移动
if (isCropMode || isFilterMode) {
return super.onTouchEvent(event)
}
// 先处理缩放手势
scaleGestureDetector.onTouchEvent(event)
// 处理惯性滑动
gestureDetector.onTouchEvent(event)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
// 取消正在进行的惯性动画
cancelFlingAnimation()
activePointerId = event.getPointerId(0)
lastTouchX = event.x
lastTouchY = event.y
isScaling = false
}
MotionEvent.ACTION_MOVE -> {
// 只有单指且不在缩放时才处理平移
if (!scaleGestureDetector.isInProgress && !isScaling && event.pointerCount == 1) {
val dx = event.x - lastTouchX
val dy = event.y - lastTouchY
applyTranslation(dx, dy)
lastTouchX = event.x
lastTouchY = event.y
}
}
// ...
}
return true
}
}/**
* 缩放手势监听器 - 支持焦点缩放
*/
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
// 记录缩放开始时的焦点位置
lastFocusX = detector.focusX
lastFocusY = detector.focusY
isScaling = true
return true
}
override fun onScale(detector: ScaleGestureDetector): Boolean {
val scaleFactor = detector.scaleFactor
val oldScale = renderer.scaleFactor
// 计算新的缩放因子(限制范围 0.1x ~ 5.0x)
var newScale = oldScale * scaleFactor
newScale = newScale.coerceIn(0.1f, 5.0f)
// 计算焦点在 OpenGL 坐标系中的位置
val focusX = detector.focusX
val focusY = detector.focusY
// 将屏幕焦点转换为归一化坐标 (-1 到 1)
val normalizedFocusX = (focusX / glSurfaceView.width) * 2f - 1f
val normalizedFocusY = 1f - (focusY / glSurfaceView.height) * 2f
// 计算缩放前焦点在图片坐标系中的位置
val imageX = (normalizedFocusX - renderer.offsetX * oldScale) / oldScale
val imageY = (normalizedFocusY - renderer.offsetY * oldScale) / oldScale
// 应用新的缩放因子
renderer.scaleFactor = newScale
// 调整偏移量,使焦点位置保持不变
renderer.offsetX = (normalizedFocusX - imageX * newScale) / newScale
renderer.offsetY = (normalizedFocusY - imageY * newScale) / newScale
glSurfaceView.requestRender()
return true
}
}/**
* 执行惯性滑动动画
*/
private fun performFling(velocityX: Float, velocityY: Float) {
cancelFlingAnimation()
// 计算惯性滑动的初始速度(转换为 OpenGL 坐标系)
val initialVelocityX = velocityX / glSurfaceView.width / renderer.scaleFactor
val initialVelocityY = -velocityY / glSurfaceView.height / renderer.scaleFactor
flingAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
duration = 500L
interpolator = DecelerateInterpolator(2f)
var lastFraction = 1f
addUpdateListener { animator ->
val fraction = animator.animatedValue as Float
val delta = lastFraction - fraction
lastFraction = fraction
// 应用衰减的速度
renderer.offsetX += initialVelocityX * delta * duration / 1000f
renderer.offsetY += initialVelocityY * delta * duration / 1000f
applyBoundaryConstraints()
glSurfaceView.requestRender()
}
start()
}
}裁剪功能通过自定义 View CropOverlayView 实现,覆盖在 GLSurfaceView 之上,提供:
- 可视化裁剪框:带九宫格辅助线和四角标记
- 多种预设比例:自由、1:1、3:4、4:3、9:16、16:9
- 交互式调整:支持拖拽移动和边角调整大小
- 边界约束:裁剪框限制在图片显示区域内
2.3.2 裁剪比例枚举 (CropOverlayView.kt)
enum class AspectRatio(val ratio: Float, val displayName: String) {
FREE(0f, "自由"),
RATIO_1_1(1f, "1:1"),
RATIO_3_4(3f / 4f, "3:4"),
RATIO_4_3(4f / 3f, "4:3"),
RATIO_9_16(9f / 16f, "9:16"),
RATIO_16_9(16f / 9f, "16:9")
}override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val w = width.toFloat()
val h = height.toFloat()
// 1. 绘制半透明遮罩(裁剪框外的区域)
canvas.drawRect(0f, 0f, w, cropRect.top, overlayPaint)
canvas.drawRect(0f, cropRect.bottom, w, h, overlayPaint)
canvas.drawRect(0f, cropRect.top, cropRect.left, cropRect.bottom, overlayPaint)
canvas.drawRect(cropRect.right, cropRect.top, w, cropRect.bottom, overlayPaint)
// 2. 绘制裁剪框边框
canvas.drawRect(cropRect, borderPaint)
// 3. 绘制九宫格线
val gridWidth = cropRect.width() / 3
val gridHeight = cropRect.height() / 3
// 垂直线
canvas.drawLine(cropRect.left + gridWidth, cropRect.top,
cropRect.left + gridWidth, cropRect.bottom, gridPaint)
canvas.drawLine(cropRect.left + gridWidth * 2, cropRect.top,
cropRect.left + gridWidth * 2, cropRect.bottom, gridPaint)
// 水平线
canvas.drawLine(cropRect.left, cropRect.top + gridHeight,
cropRect.right, cropRect.top + gridHeight, gridPaint)
canvas.drawLine(cropRect.left, cropRect.top + gridHeight * 2,
cropRect.right, cropRect.top + gridHeight * 2, gridPaint)
// 4. 绘制四个角的标记
val cornerSize = 30f
// 左上角
canvas.drawLine(cropRect.left, cropRect.top,
cropRect.left + cornerSize, cropRect.top, cornerPaint)
canvas.drawLine(cropRect.left, cropRect.top,
cropRect.left, cropRect.top + cornerSize, cornerPaint)
// ... 其他三个角
}override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
lastTouchX = event.x
lastTouchY = event.y
touchMode = getTouchMode(event.x, event.y)
return true
}
MotionEvent.ACTION_MOVE -> {
val dx = event.x - lastTouchX
val dy = event.y - lastTouchY
when (touchMode) {
TouchMode.DRAG -> moveCropRect(dx, dy)
TouchMode.TOP_LEFT -> resizeCropRect(dx, dy, true, true, false, false)
TouchMode.TOP_RIGHT -> resizeCropRect(dx, dy, false, true, true, false)
TouchMode.BOTTOM_LEFT -> resizeCropRect(dx, dy, true, false, false, true)
TouchMode.BOTTOM_RIGHT -> resizeCropRect(dx, dy, false, false, true, true)
TouchMode.LEFT -> resizeCropRect(dx, 0f, true, false, false, false)
TouchMode.RIGHT -> resizeCropRect(dx, 0f, false, false, true, false)
TouchMode.TOP -> resizeCropRect(0f, dy, false, true, false, false)
TouchMode.BOTTOM -> resizeCropRect(0f, dy, false, false, false, true)
else -> {}
}
lastTouchX = event.x
lastTouchY = event.y
invalidate()
return true
}
// ...
}
return super.onTouchEvent(event)
}/**
* 按比例调整大小
* 拖动角点或边缘时,保持裁剪框的宽高比不变
*/
private fun resizeWithAspectRatio(rect: RectF, dx: Float, dy: Float,
left: Boolean, top: Boolean, right: Boolean, bottom: Boolean) {
val ratio = currentAspectRatio.ratio
val centerX = rect.centerX()
val centerY = rect.centerY()
when {
// 四个角点 - 对角线调整,保持中心点
(left && top) || (right && bottom) -> {
val effectiveDelta = maxOf(abs(dx), abs(dy) * ratio)
val direction = if ((left && top && dx < 0) || (right && bottom && dx > 0)) -1f else 1f
val newWidth = rect.width() + effectiveDelta * direction * 2
val newHeight = newWidth / ratio
rect.set(
centerX - newWidth / 2,
centerY - newHeight / 2,
centerX + newWidth / 2,
centerY + newHeight / 2
)
}
// 边缘拖动 - 从中心等比例缩放
left || right -> {
val widthDelta = if (left) -dx * 2 else dx * 2
val newWidth = rect.width() + widthDelta
val newHeight = newWidth / ratio
rect.set(
centerX - newWidth / 2,
centerY - newHeight / 2,
centerX + newWidth / 2,
centerY + newHeight / 2
)
}
// ...
}
}撤销/恢复功能通过维护一个编辑状态历史栈实现:
- 状态快照:每次编辑操作前保存当前状态
- 历史栈管理:使用列表存储历史状态,支持前进/后退
- 内存优化:限制最大历史记录数量(默认 50 条)
- Bitmap 历史:对于裁剪等破坏性操作,额外保存 Bitmap 历史
2.4.2 编辑状态数据类 (EditHistory.kt)
data class EditState(
val adjustmentParams: AdjustmentParams, // 调整参数(亮度、对比度等)
val scaleFactor: Float, // 缩放因子
val offsetX: Float, // X 偏移
val offsetY: Float, // Y 偏移
val rotationAngle: Float, // 旋转角度
val cropRect: RectF?, // 裁剪区域
val isGrayscaleEnabled: Boolean, // 灰度开关
val filterType: FilterType, // 滤镜类型
val bitmapHistoryIndex: Int = -1, // Bitmap 历史索引(用于裁剪撤销)
val imageWidth: Int = 0, // 当前图片宽度
val imageHeight: Int = 0, // 当前图片高度
val timestamp: Long = System.currentTimeMillis()
) {
fun copy(): EditState {
return EditState(
adjustmentParams = adjustmentParams.copy(),
scaleFactor = scaleFactor,
offsetX = offsetX,
offsetY = offsetY,
rotationAngle = rotationAngle,
cropRect = cropRect?.let { RectF(it) },
isGrayscaleEnabled = isGrayscaleEnabled,
filterType = filterType,
bitmapHistoryIndex = bitmapHistoryIndex,
imageWidth = imageWidth,
imageHeight = imageHeight,
timestamp = timestamp
)
}
}class EditHistory(private val maxHistorySize: Int = 50) {
private val history = mutableListOf<EditState>()
private var currentIndex = -1
/**
* 添加新状态到历史记录
*/
fun addState(state: EditState) {
// 如果当前不在历史末尾,删除当前位置之后的所有状态
if (currentIndex < history.size - 1) {
history.subList(currentIndex + 1, history.size).clear()
}
// 添加新状态
history.add(state.copy())
currentIndex = history.size - 1
// 如果超过最大历史记录数,删除最旧的记录
if (history.size > maxHistorySize) {
history.removeAt(0)
currentIndex--
}
}
/**
* 撤销 - 返回上一个状态
*/
fun undo(): EditState? {
if (!canUndo()) return null
currentIndex--
return history[currentIndex].copy()
}
/**
* 恢复 - 返回下一个状态
*/
fun redo(): EditState? {
if (!canRedo()) return null
currentIndex++
return history[currentIndex].copy()
}
fun canUndo(): Boolean = currentIndex > 0
fun canRedo(): Boolean = currentIndex < history.size - 1
}/**
* 恢复编辑状态
*/
private fun restoreEditState(state: EditState) {
// 1. 恢复 Bitmap 历史(用于裁剪撤销)
if (state.bitmapHistoryIndex >= 0) {
val currentBitmapIndex = renderer.getBitmapHistoryIndex()
if (currentBitmapIndex != state.bitmapHistoryIndex) {
renderer.restoreBitmapHistory(state.bitmapHistoryIndex)
}
}
// 2. 恢复调整参数
renderer.adjustmentParams = state.adjustmentParams.copy()
adjustmentAdapter.updateValues()
// 3. 恢复变换参数
renderer.scaleFactor = state.scaleFactor
renderer.offsetX = state.offsetX
renderer.offsetY = state.offsetY
// 4. 恢复旋转角度
renderer.setRotation(state.rotationAngle)
// 5. 清除裁剪区域(裁剪已经通过 Bitmap 历史恢复)
renderer.clearCropRect()
// 6. 恢复滤镜
renderer.isGrayscaleEnabled = state.isGrayscaleEnabled
renderer.currentFilter = state.filterType
filterAdapter.setSelectedFilter(state.filterType)
// 7. 重新渲染
glSurfaceView.requestRender()
// 8. 更新按钮状态
updateUndoRedoButtons()
}/**
* 更新撤销/恢复按钮状态
*/
private fun updateUndoRedoButtons() {
findViewById<ImageButton>(R.id.undo_button).isEnabled = editHistory.canUndo()
findViewById<ImageButton>(R.id.redo_button).isEnabled = editHistory.canRedo()
}
/**
* 执行撤销操作
*/
private fun performUndo() {
val previousState = editHistory.undo()
if (previousState != null) {
restoreEditState(previousState)
}
}
/**
* 执行恢复操作
*/
private fun performRedo() {
val nextState = editHistory.redo()
if (nextState != null) {
restoreEditState(nextState)
}
}滤镜功能基于 OpenGL ES 2.0 的片段着色器(Fragment Shader)实现,每种滤镜对应一个独立的 GLSL 着色器程序:
- 预编译着色器:在
onSurfaceCreated时为每种滤镜编译着色器程序 - 实时切换:切换滤镜时只需切换着色器程序,无需重新加载纹理
- GPU 加速:所有颜色变换在 GPU 上并行执行,性能优异
2.5.2 滤镜类型枚举 (FilterType.kt)
enum class FilterType(val displayName: String, val fragmentShader: String) {
NONE("原图", """
// 原图着色器,支持亮度、对比度、饱和度等调整
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_Texture;
uniform float u_Brightness;
uniform float u_Contrast;
uniform float u_Saturation;
// ... 其他调整参数
void main(){
vec4 color = texture2D(u_Texture, v_TexCoord);
vec3 rgb = color.rgb;
// 亮度调整
rgb += u_Brightness * 0.5;
// 对比度调整
rgb = (rgb - 0.5) * (1.0 + u_Contrast) + 0.5;
// 饱和度调整(HSV 转换)
vec3 hsv = rgb2hsv(rgb);
hsv.y *= (1.0 + u_Saturation);
rgb = hsv2rgb(hsv);
gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), color.a);
}
"""),
GRAYSCALE("黑白", """
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_Texture;
void main(){
vec4 color = texture2D(u_Texture, v_TexCoord);
// 使用 ITU-R BT.601 标准的亮度权重
float gray = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
gl_FragColor = vec4(gray, gray, gray, color.a);
}
"""),
SEPIA("复古", """
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_Texture;
void main(){
vec4 color = texture2D(u_Texture, v_TexCoord);
// 复古色调矩阵变换
float r = color.r * 0.393 + color.g * 0.769 + color.b * 0.189;
float g = color.r * 0.349 + color.g * 0.686 + color.b * 0.168;
float b = color.r * 0.272 + color.g * 0.534 + color.b * 0.131;
gl_FragColor = vec4(r, g, b, color.a);
}
"""),
COOL("冷色调", """..."""),
WARM("暖色调", """..."""),
VIVID("鲜艳", """..."""),
FADE("淡化", """..."""),
INVERT("反色", """..."""),
BRIGHTNESS("增亮", """..."""),
CONTRAST("对比度", """...""");
}2.5.3 滤镜程序管理 (ImageRenderer.kt)
class ImageRenderer : GLSurfaceView.Renderer {
// 存储所有滤镜的程序ID
private val filterPrograms = mutableMapOf<FilterType, Int>()
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 为每个滤镜创建着色器程序
val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
FilterType.values().forEach { filter ->
val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, filter.fragmentShader)
val program = GLES20.glCreateProgram().also {
GLES20.glAttachShader(it, vertexShader)
GLES20.glAttachShader(it, fragmentShader)
GLES20.glLinkProgram(it)
}
filterPrograms[filter] = program
}
}
override fun onDrawFrame(gl: GL10?) {
// 根据当前滤镜选择程序
val activeFilter = if (isGrayscaleEnabled) FilterType.GRAYSCALE else currentFilter
programId = filterPrograms[activeFilter] ?: filterPrograms[FilterType.NONE] ?: 0
GLES20.glUseProgram(programId)
// 传递调整参数到着色器(仅对 NONE 滤镜有效)
if (activeFilter == FilterType.NONE) {
GLES20.glUniform1f(brightnessHandle, adjustmentParams.brightness)
GLES20.glUniform1f(contrastHandle, adjustmentParams.contrast)
GLES20.glUniform1f(saturationHandle, adjustmentParams.saturation)
// ... 其他参数
}
// 绑定纹理并绘制
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
}
}| 滤镜名称 | 效果描述 | 实现原理 |
|---|---|---|
| 原图 | 无滤镜,支持参数调整 | 直接输出纹理颜色,支持亮度/对比度/饱和度等调整 |
| 黑白 | 灰度效果 | 使用 BT.601 标准计算亮度值 |
| 复古 | 怀旧棕褐色调 | 应用 Sepia 色调矩阵变换 |
| 冷色调 | 偏蓝色调 | 增强蓝色通道,减弱红色通道 |
| 暖色调 | 偏橙色调 | 增强红色通道,减弱蓝色通道 |
| 鲜艳 | 高饱和度 | 增加饱和度系数(1.5x) |
| 淡化 | 低饱和度 | 降低饱和度系数(0.5x) |
| 反色 | 颜色反转 | RGB 各通道取反(1.0 - color) |
| 增亮 | 整体提亮 | RGB 各通道加固定值 |
| 对比度 | 增强对比 | 以 0.5 为中心拉伸颜色范围 |
图片旋转通过纹理坐标变换实现,而非矩阵旋转。这种方式可以避免旋转 90° 时图片变形的问题:
- 纹理坐标映射:根据旋转角度重新映射纹理坐标
- 顶点缓冲更新:旋转后重新计算顶点坐标以适应新的宽高比
- 90° 增量旋转:只支持 0°、90°、180°、270° 四个角度
/**
* 判断当前旋转角度是否为90度或270度(即宽高互换的情况)
*/
private fun isRotated90or270(): Boolean {
val normalizedAngle = ((rotationAngle % 360) + 360) % 360
return normalizedAngle == 90f || normalizedAngle == 270f
}
/**
* 获取旋转后的有效图片宽度
*/
private fun getEffectiveImageWidth(): Int {
return if (isRotated90or270()) imageHeight else imageWidth
}
/**
* 获取旋转后的有效图片高度
*/
private fun getEffectiveImageHeight(): Int {
return if (isRotated90or270()) imageWidth else imageHeight
}/**
* 根据旋转角度更新纹理坐标
* 通过调整纹理坐标来实现旋转效果
*/
private fun updateTextureCoordinates() {
val normalizedAngle = ((rotationAngle % 360) + 360) % 360
// 根据旋转角度设置纹理坐标
// 顶点顺序:左下、右下、左上、右上
textureData = when (normalizedAngle.toInt()) {
90 -> floatArrayOf(
// 顺时针旋转90度
1.0f, 1.0f, // 左下顶点 -> 原右下纹理
1.0f, 0.0f, // 右下顶点 -> 原右上纹理
0.0f, 1.0f, // 左上顶点 -> 原左下纹理
0.0f, 0.0f // 右上顶点 -> 原左上纹理
)
180 -> floatArrayOf(
// 旋转180度
1.0f, 0.0f, // 左下顶点 -> 原右上纹理
0.0f, 0.0f, // 右下顶点 -> 原左上纹理
1.0f, 1.0f, // 左上顶点 -> 原右下纹理
0.0f, 1.0f // 右上顶点 -> 原左下纹理
)
270 -> floatArrayOf(
// 顺时针旋转270度(逆时针90度)
0.0f, 0.0f, // 左下顶点 -> 原左上纹理
0.0f, 1.0f, // 右下顶点 -> 原左下纹理
1.0f, 0.0f, // 左上顶点 -> 原右上纹理
1.0f, 1.0f // 右上顶点 -> 原右下纹理
)
else -> floatArrayOf(
// 0度:正常
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f
)
}
textureBuffer = ByteBuffer.allocateDirect(textureData.size * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(textureData)
textureBuffer.position(0)
}/**
* 根据当前旋转角度更新顶点缓冲区
* 旋转90度或270度时,图片的宽高比会互换
*/
private fun updateVertexBuffer() {
if (imageWidth == 0 || imageHeight == 0 || viewportWidth == 0 || viewportHeight == 0) return
val screenRatio = viewportWidth.toFloat() / viewportHeight.toFloat()
// 根据旋转角度确定有效的图片宽高比
val effectiveWidth = getEffectiveImageWidth()
val effectiveHeight = getEffectiveImageHeight()
val imageRatio = effectiveWidth.toFloat() / effectiveHeight.toFloat()
var left = -1.0f
var right = 1.0f
var bottom = -1.0f
var top = 1.0f
// 根据宽高比计算顶点坐标,保持图片比例
if (screenRatio > imageRatio) {
// 视口更宽,根据高度缩放
val newWidth = imageRatio / screenRatio
left = -newWidth
right = newWidth
} else {
// 视口更高,根据宽度缩放
val newHeight = screenRatio / imageRatio
bottom = -newHeight
top = newHeight
}
val newVertexData = floatArrayOf(
left, bottom,
right, bottom,
left, top,
right, top
)
vertexBuffer.clear()
vertexBuffer.put(newVertexData)
vertexBuffer.position(0)
// 更新纹理坐标
updateTextureCoordinates()
}2.6.5 旋转操作触发 (EditorActivity.kt)
/**
* 旋转图片(增量旋转)
* @param degrees 旋转角度,正值为顺时针,负值为逆时针
*/
private fun rotateImage(degrees: Float) {
// 保存当前状态到历史记录
saveCurrentStateToHistory()
// 获取当前角度并增加旋转
val currentAngle = renderer.getRotation()
var newAngle = currentAngle + degrees
// 将角度规范化到 0-360 范围内
while (newAngle >= 360f) newAngle -= 360f
while (newAngle < 0f) newAngle += 360f
// 只保留 0, 90, 180, 270 四个角度
newAngle = when {
newAngle < 45f -> 0f
newAngle < 135f -> 90f
newAngle < 225f -> 180f
newAngle < 315f -> 270f
else -> 0f
}
renderer.setRotation(newAngle)
glSurfaceView.requestRender()
autoSaveDraft()
}基础参数调整功能允许用户实时调整图片的亮度、对比度、饱和度等 9 种参数。所有调整都通过 OpenGL ES 片段着色器在 GPU 上实时计算,确保流畅的预览体验。
2.7.2 调整参数数据类 (AdjustmentParams.kt)
/**
* 图片调整参数数据类
* 所有参数范围为 -1.0 到 1.0,默认值为 0.0
*/
data class AdjustmentParams(
var brightness: Float = 0f, // 亮度
var contrast: Float = 0f, // 对比度
var saturation: Float = 0f, // 饱和度
var highlights: Float = 0f, // 高光
var shadows: Float = 0f, // 阴影
var temperature: Float = 0f, // 色温
var tint: Float = 0f, // 色调
var clarity: Float = 0f, // 清晰度
var sharpness: Float = 0f // 锐化
) {
fun copy(): AdjustmentParams {
return AdjustmentParams(
brightness, contrast, saturation,
highlights, shadows, temperature,
tint, clarity, sharpness
)
}
fun reset() {
brightness = 0f
contrast = 0f
saturation = 0f
highlights = 0f
shadows = 0f
temperature = 0f
tint = 0f
clarity = 0f
sharpness = 0f
}
}enum class AdjustmentType(val displayName: String, val iconRes: Int) {
BRIGHTNESS("亮度", R.drawable.ic_brightness),
CONTRAST("对比度", R.drawable.ic_contrast),
SATURATION("饱和度", R.drawable.ic_saturation),
HIGHLIGHTS("高光", R.drawable.ic_highlights),
SHADOWS("阴影", R.drawable.ic_shadows),
TEMPERATURE("色温", R.drawable.ic_temperature),
TINT("色调", R.drawable.ic_tint),
CLARITY("清晰度", R.drawable.ic_clarity),
SHARPNESS("锐化", R.drawable.ic_sharpness)
}| 参数 | 效果描述 | 实现原理 |
|---|---|---|
| 亮度 | 整体明暗调整 | RGB 各通道加减固定值 |
| 对比度 | 明暗对比强度 | 以 0.5 为中心拉伸/压缩颜色范围 |
| 饱和度 | 色彩鲜艳程度 | HSV 空间调整 S 分量 |
| 高光 | 亮部区域调整 | 仅影响亮度值 > 0.5 的像素 |
| 阴影 | 暗部区域调整 | 仅影响亮度值 < 0.5 的像素 |
| 色温 | 冷暖色调调整 | 调整红蓝通道比例 |
| 色调 | 绿品红偏移 | 调整绿品红通道比例 |
| 清晰度 | 局部对比度增强 | 高通滤波 + 叠加 |
| 锐化 | 边缘增强 | 拉普拉斯卷积核 |
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_Texture;
// 调整参数 uniform
uniform float u_Brightness;
uniform float u_Contrast;
uniform float u_Saturation;
uniform float u_Highlights;
uniform float u_Shadows;
uniform float u_Temperature;
uniform float u_Tint;
uniform float u_Clarity;
uniform float u_Sharpness;
// RGB 转 HSV
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0/3.0, 2.0/3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
// HSV 转 RGB
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
vec4 color = texture2D(u_Texture, v_TexCoord);
vec3 rgb = color.rgb;
// 1. 亮度调整
rgb += u_Brightness * 0.5;
// 2. 对比度调整
rgb = (rgb - 0.5) * (1.0 + u_Contrast) + 0.5;
// 3. 饱和度调整(HSV 空间)
vec3 hsv = rgb2hsv(rgb);
hsv.y *= (1.0 + u_Saturation);
rgb = hsv2rgb(hsv);
// 4. 高光调整(仅影响亮部)
float luminance = dot(rgb, vec3(0.299, 0.587, 0.114));
float highlightMask = smoothstep(0.5, 1.0, luminance);
rgb += u_Highlights * 0.3 * highlightMask;
// 5. 阴影调整(仅影响暗部)
float shadowMask = 1.0 - smoothstep(0.0, 0.5, luminance);
rgb += u_Shadows * 0.3 * shadowMask;
// 6. 色温调整(红蓝通道)
rgb.r += u_Temperature * 0.1;
rgb.b -= u_Temperature * 0.1;
// 7. 色调调整(绿品红通道)
rgb.g += u_Tint * 0.1;
// 8. 清晰度(简化实现:局部对比度)
rgb = mix(rgb, rgb * rgb * (3.0 - 2.0 * rgb), u_Clarity * 0.5);
// 9. 锐化(简化实现:边缘增强)
// 完整实现需要多次纹理采样,这里使用简化版本
gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), color.a);
}2.7.6 调整面板适配器 (AdjustmentAdapter.kt)
class AdjustmentAdapter(
private val onAdjustmentChanged: (AdjustmentType, Float) -> Unit
) : RecyclerView.Adapter<AdjustmentAdapter.ViewHolder>() {
private val adjustmentTypes = AdjustmentType.values().toList()
private var selectedPosition = 0
private var currentParams = AdjustmentParams()
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val iconView: ImageView = itemView.findViewById(R.id.adjustment_icon)
val nameView: TextView = itemView.findViewById(R.id.adjustment_name)
val valueView: TextView = itemView.findViewById(R.id.adjustment_value)
val slider: SeekBar = itemView.findViewById(R.id.adjustment_slider)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val type = adjustmentTypes[position]
holder.iconView.setImageResource(type.iconRes)
holder.nameView.text = type.displayName
// 获取当前参数值
val currentValue = getParamValue(type)
holder.valueView.text = formatValue(currentValue)
// 设置滑块(范围 0-200,中间值 100 对应参数 0)
holder.slider.progress = ((currentValue + 1f) * 100).toInt()
holder.slider.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
val newValue = (progress - 100) / 100f
setParamValue(type, newValue)
holder.valueView.text = formatValue(newValue)
onAdjustmentChanged(type, newValue)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
}
private fun getParamValue(type: AdjustmentType): Float {
return when (type) {
AdjustmentType.BRIGHTNESS -> currentParams.brightness
AdjustmentType.CONTRAST -> currentParams.contrast
AdjustmentType.SATURATION -> currentParams.saturation
AdjustmentType.HIGHLIGHTS -> currentParams.highlights
AdjustmentType.SHADOWS -> currentParams.shadows
AdjustmentType.TEMPERATURE -> currentParams.temperature
AdjustmentType.TINT -> currentParams.tint
AdjustmentType.CLARITY -> currentParams.clarity
AdjustmentType.SHARPNESS -> currentParams.sharpness
}
}
}// EditorActivity.kt 中的参数变化处理
private fun setupAdjustmentPanel() {
adjustmentAdapter = AdjustmentAdapter { type, value ->
// 更新渲染器参数
when (type) {
AdjustmentType.BRIGHTNESS -> renderer.adjustmentParams.brightness = value
AdjustmentType.CONTRAST -> renderer.adjustmentParams.contrast = value
AdjustmentType.SATURATION -> renderer.adjustmentParams.saturation = value
AdjustmentType.HIGHLIGHTS -> renderer.adjustmentParams.highlights = value
AdjustmentType.SHADOWS -> renderer.adjustmentParams.shadows = value
AdjustmentType.TEMPERATURE -> renderer.adjustmentParams.temperature = value
AdjustmentType.TINT -> renderer.adjustmentParams.tint = value
AdjustmentType.CLARITY -> renderer.adjustmentParams.clarity = value
AdjustmentType.SHARPNESS -> renderer.adjustmentParams.sharpness = value
}
// 请求重新渲染
glSurfaceView.requestRender()
// 防抖保存状态(避免频繁保存)
adjustmentDebounceJob?.cancel()
adjustmentDebounceJob = lifecycleScope.launch {
delay(500) // 500ms 防抖
saveCurrentStateToHistory()
autoSaveDraft()
}
}
adjustmentRecyclerView.adapter = adjustmentAdapter
adjustmentRecyclerView.layoutManager = LinearLayoutManager(
this, LinearLayoutManager.HORIZONTAL, false
)
}注:截图由用户补充
- 双指缩放效果
- 单指拖拽平移效果
- 惯性滑动效果
- 裁剪模式界面
- 不同比例选择(1:1、3:4、9:16 等)
- 裁剪框调整交互
- 撤销按钮状态变化
- 恢复按钮状态变化
- 多步操作撤销演示
- 滤镜选择界面
- 各种滤镜效果预览
- 旋转按钮
- 90° 增量旋转效果
| 任务 | 功能点 | 完成状态 |
|---|---|---|
| P0 | 自定义 Gradle 插件 - 编译前清除缓存 | ✅ 已完成 |
| P2 | 手势操作 - 双指缩放、单指拖拽 | ✅ 已完成 |
| P2 | 比例裁剪 - 多种固定比例、可调整裁剪框 | ✅ 已完成 |
| P2 | Undo/Redo - 操作撤销与恢复 | ✅ 已完成 |
| P2 | 滤镜效果 - 10 种预设滤镜 | ✅ 已完成 |
| P2 | 图片旋转 - 90° 增量旋转 | ✅ 已完成 |
| P2 | 基础参数调整 - 9 种参数实时调整 | ✅ 已完成 |
- 焦点缩放:缩放时以双指中心为焦点,提供自然的缩放体验
- 惯性滑动:使用
DecelerateInterpolator实现平滑的惯性动画 - 固定比例裁剪:支持从中心等比例缩放,保持裁剪框比例不变
- Bitmap 历史管理:针对裁剪等破坏性操作,额外维护 Bitmap 历史栈
- 状态快照:完整保存编辑状态,支持精确恢复
- GPU 滤镜渲染:基于 OpenGL ES 着色器实现滤镜,GPU 并行处理,性能优异
- 纹理坐标旋转:通过纹理坐标变换实现旋转,避免矩阵旋转导致的变形问题
- 实时参数调整:9 种图片参数通过 GLSL 着色器在 GPU 上实时计算,流畅预览
- 防抖保存机制:参数调整时使用 500ms 防抖,避免频繁保存历史状态