Skip to content

Latest commit

 

History

History
1249 lines (1020 loc) · 41.5 KB

File metadata and controls

1249 lines (1020 loc) · 41.5 KB

图片编辑应用 - 进阶项目报告

概述

本文档记录了在基础项目完成后,进一步实现的进阶功能。进阶任务包括工程化插件开发(P0)和图像编辑功能实现(P2)。


P0. 工程化插件开发

1.1 功能目标

编写一个自定义 Gradle 插件,为项目增加自动化构建能力:在 App 编译前清除指定目录下的缓存

1.2 实现方案

app/build.gradle.kts 中定义了一个自定义 Task cleanCacheDir,并将其配置为 preBuild 任务的依赖,确保每次编译前自动执行缓存清理。

1.3 脚本文件

build.gradle.kts 配置

// 定义清除缓存的 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")
}

1.4 实现思路

  1. Task 注册:使用 tasks.register() 方法注册一个名为 cleanCacheDir 的自定义任务
  2. 任务分组:将任务归类到 cache 组,便于在 Gradle 任务列表中查找
  3. 缓存目录配置:定义需要清理的缓存目录列表,包括:
    • build/intermediates/transforms:中间转换文件
    • build/tmp:临时文件
  4. 安全删除:在删除前检查目录是否存在,避免不必要的错误
  5. 任务依赖:通过 dependsOn()cleanCacheDir 设置为 preBuild 的依赖,确保编译前自动执行

1.5 运行方式

方式一:自动执行(推荐)

由于已配置为 preBuild 的依赖,每次执行编译命令时会自动执行:

./gradlew assembleDebug
#
./gradlew build

方式二:手动执行

./gradlew cleanCacheDir

1.6 运行结果

执行编译时,控制台输出示例:

> 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
...

1.7 扩展说明

该 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}")
    }
}

P2. 图像编辑初体验

2.1 功能概述

实现了六种核心编辑操作:

功能 描述 实现状态
手势操作 支持双指缩放和单指拖拽平移画布 ✅ 已实现
比例裁剪 提供多种固定裁剪比例,允许用户调整裁剪框 ✅ 已实现
Undo/Redo 支持操作的撤销与恢复 ✅ 已实现
滤镜效果 提供多种预设滤镜,基于 OpenGL ES 着色器实现 ✅ 已实现
图片旋转 支持 90° 增量旋转,通过纹理坐标变换实现 ✅ 已实现
参数调整 支持亮度、对比度、饱和度等 9 种参数实时调整 ✅ 已实现

2.2 手势操作实现

2.2.1 实现思路

手势操作通过 Android 的 ScaleGestureDetectorGestureDetector 实现,支持:

  • 双指缩放:以双指中心点为焦点进行缩放,缩放范围限制在 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
    }
}

2.2.3 焦点缩放实现

/**
 * 缩放手势监听器 - 支持焦点缩放
 */
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
    }
}

2.2.4 惯性滑动实现

/**
 * 执行惯性滑动动画
 */
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()
    }
}

2.3 比例裁剪实现

2.3.1 实现思路

裁剪功能通过自定义 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")
}

2.3.3 裁剪框绘制

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)
    // ... 其他三个角
}

2.3.4 触摸交互处理

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)
}

2.3.5 固定比例调整

/**
 * 按比例调整大小
 * 拖动角点或边缘时,保持裁剪框的宽高比不变
 */
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
            )
        }
        // ...
    }
}

2.4 Undo/Redo 实现

2.4.1 实现思路

撤销/恢复功能通过维护一个编辑状态历史栈实现:

  • 状态快照:每次编辑操作前保存当前状态
  • 历史栈管理:使用列表存储历史状态,支持前进/后退
  • 内存优化:限制最大历史记录数量(默认 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
        )
    }
}

2.4.3 历史记录管理类

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
}

2.4.4 状态恢复实现

/**
 * 恢复编辑状态
 */
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()
}

2.4.5 按钮状态更新

/**
 * 更新撤销/恢复按钮状态
 */
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)
    }
}

2.5 滤镜效果实现

2.5.1 实现思路

滤镜功能基于 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)
    }
}

2.5.4 支持的滤镜效果

滤镜名称 效果描述 实现原理
原图 无滤镜,支持参数调整 直接输出纹理颜色,支持亮度/对比度/饱和度等调整
黑白 灰度效果 使用 BT.601 标准计算亮度值
复古 怀旧棕褐色调 应用 Sepia 色调矩阵变换
冷色调 偏蓝色调 增强蓝色通道,减弱红色通道
暖色调 偏橙色调 增强红色通道,减弱蓝色通道
鲜艳 高饱和度 增加饱和度系数(1.5x)
淡化 低饱和度 降低饱和度系数(0.5x)
反色 颜色反转 RGB 各通道取反(1.0 - color)
增亮 整体提亮 RGB 各通道加固定值
对比度 增强对比 以 0.5 为中心拉伸颜色范围

2.6 图片旋转实现

2.6.1 实现思路

图片旋转通过纹理坐标变换实现,而非矩阵旋转。这种方式可以避免旋转 90° 时图片变形的问题:

  • 纹理坐标映射:根据旋转角度重新映射纹理坐标
  • 顶点缓冲更新:旋转后重新计算顶点坐标以适应新的宽高比
  • 90° 增量旋转:只支持 0°、90°、180°、270° 四个角度

2.6.2 旋转角度判断

/**
 * 判断当前旋转角度是否为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
}

2.6.3 纹理坐标更新

/**
 * 根据旋转角度更新纹理坐标
 * 通过调整纹理坐标来实现旋转效果
 */
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)
}

2.6.4 顶点缓冲更新

/**
 * 根据当前旋转角度更新顶点缓冲区
 * 旋转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()
}

2.7 基础参数调整实现

2.7.1 实现思路

基础参数调整功能允许用户实时调整图片的亮度、对比度、饱和度等 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
    }
}

2.7.3 调整类型枚举

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)
}

2.7.4 参数说明

参数 效果描述 实现原理
亮度 整体明暗调整 RGB 各通道加减固定值
对比度 明暗对比强度 以 0.5 为中心拉伸/压缩颜色范围
饱和度 色彩鲜艳程度 HSV 空间调整 S 分量
高光 亮部区域调整 仅影响亮度值 > 0.5 的像素
阴影 暗部区域调整 仅影响亮度值 < 0.5 的像素
色温 冷暖色调调整 调整红蓝通道比例
色调 绿品红偏移 调整绿品红通道比例
清晰度 局部对比度增强 高通滤波 + 叠加
锐化 边缘增强 拉普拉斯卷积核

2.7.5 GLSL 着色器实现

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
        }
    }
}

2.7.7 参数变化回调处理

// 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
    )
}

2.8 功能截图

注:截图由用户补充

2.7.1 手势操作

  • 双指缩放效果
  • 单指拖拽平移效果
  • 惯性滑动效果

2.7.2 比例裁剪

  • 裁剪模式界面
  • 不同比例选择(1:1、3:4、9:16 等)
  • 裁剪框调整交互

2.7.3 Undo/Redo

  • 撤销按钮状态变化
  • 恢复按钮状态变化
  • 多步操作撤销演示

2.7.4 滤镜效果

  • 滤镜选择界面
  • 各种滤镜效果预览

2.7.5 图片旋转

  • 旋转按钮
  • 90° 增量旋转效果

总结

进阶功能完成情况

任务 功能点 完成状态
P0 自定义 Gradle 插件 - 编译前清除缓存 ✅ 已完成
P2 手势操作 - 双指缩放、单指拖拽 ✅ 已完成
P2 比例裁剪 - 多种固定比例、可调整裁剪框 ✅ 已完成
P2 Undo/Redo - 操作撤销与恢复 ✅ 已完成
P2 滤镜效果 - 10 种预设滤镜 ✅ 已完成
P2 图片旋转 - 90° 增量旋转 ✅ 已完成
P2 基础参数调整 - 9 种参数实时调整 ✅ 已完成

技术亮点

  1. 焦点缩放:缩放时以双指中心为焦点,提供自然的缩放体验
  2. 惯性滑动:使用 DecelerateInterpolator 实现平滑的惯性动画
  3. 固定比例裁剪:支持从中心等比例缩放,保持裁剪框比例不变
  4. Bitmap 历史管理:针对裁剪等破坏性操作,额外维护 Bitmap 历史栈
  5. 状态快照:完整保存编辑状态,支持精确恢复
  6. GPU 滤镜渲染:基于 OpenGL ES 着色器实现滤镜,GPU 并行处理,性能优异
  7. 纹理坐标旋转:通过纹理坐标变换实现旋转,避免矩阵旋转导致的变形问题
  8. 实时参数调整:9 种图片参数通过 GLSL 着色器在 GPU 上实时计算,流畅预览
  9. 防抖保存机制:参数调整时使用 500ms 防抖,避免频繁保存历史状态