简介这是一份面向Android中高级开发者的UI组件学习资源聚焦移动端专业级颜色选择器的实现原理与工程实践解决移动应用中缺乏Photoshop式精细调色能力的痛点适用于图像编辑、设计工具类App开发及自定义视图进阶学习。压缩包共58个文件含21个class字节码与8个java源码核心为ColorPanel自定义控件、7个xml布局与资源定义、8个png图标素材以及APK可运行演示、proguard混淆配置、Git工程元数据等整体仅330KB轻量易集成。已有831人学习下载说明其在小而精的UI组件实践中具备较强参考价值。读者可直接复用ColorPanel模块深入理解HSV色轮交互、RGB/HSV实时转换算法、滑块与触摸事件协同响应机制并通过源码掌握Android自定义View的onDraw与onTouchEvent深度定制技巧是提升色彩处理能力与UI工程化水平的优质范例。1. 为什么一个“仿PhotoShop调色板”的Android组件比写十个列表页更考验工程直觉你有没有试过在Android App里加一个「饱和度滑块」——用户拖动时UI要实时响应、数值要精确映射到图像处理管线、还要兼容低端机不卡顿结果发现滑块一动预览图延迟半秒换台旧手机颜色直接偏绿导出图片后HSL值和滑块位置对不上……这不是玄学是调色逻辑没闭环。“Android 仿PhotoShop调色板”不是做个带红绿蓝滑块的ViewGroup就完事。它本质是一个轻量级图像处理控制中枢前端要承载HSV/HSL/RGB多维参数交互含色相环、吸管、预设滤镜入口中层要桥接Android原生ColorMatrix、RenderScript或CameraX的YUV/RGB转换链路后端还得确保参数变更能原子化同步到渲染线程——漏掉任一环用户就会觉得“这调色器不跟手”“颜色不准”“点一下变三样”。适合谁不是刚学SeekBar的新手而是已做过Camera预览、自定义View、至少一次Bitmap内存优化的开发者。如果你正为某图像编辑Demo卡在“调色功能像PPT翻页”或想把离线滤镜能力封装成可复用模块这篇就是你该停下来的实操笔记。我们不造轮子只拆解怎么让轮子咬合得严丝合缝。2. 从零搭起调色板骨架View层级设计与参数模型选型2.1 为什么放弃ConstraintLayout嵌套而用自定义ViewGroup承载所有控件常见误区是用一堆SeekBarTextView堆出RGB滑块再加个ImageView做预览。问题立刻暴露滑块拖动时onProgressChanged频繁触发若每次都在主线程setImageBitmap()低端机直接掉帧RGB三通道独立滑块无法表达色相环旋转这种极坐标操作用户调完亮度又去调对比度两次ColorMatrix.setScale()叠加顺序错乱结果不可逆。我的做法用继承ViewGroup的ColorPalettePanel作为根容器内部只持有一个ColorPreviewView继承SurfaceView或TextureView和一个ColorControlLayer继承View负责绘制色相环、滑块轨道、吸管图标等。所有交互事件触摸、拖拽、点击由ColorControlLayer捕获并转换为统一参数对象再通过接口回调通知ColorPreviewView重绘。// ColorPalettePanel.java public class ColorPalettePanel extends ViewGroup { private ColorPreviewView previewView; private ColorControlLayer controlLayer; public ColorPalettePanel(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { // 预览视图必须用SurfaceView/TextureView避免Bitmap频繁GC previewView new ColorPreviewView(getContext()); controlLayer new ColorControlLayer(getContext()); // 关键预览视图占满父容器控制层覆盖其上z-order addView(previewView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); addView(controlLayer, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // 强制两个子View尺寸完全一致 int width r - l; int height b - t; previewView.layout(0, 0, width, height); controlLayer.layout(0, 0, width, height); } }提示SurfaceView比TextureView更适合实时预览——它拥有独立Surface渲染线程可直接写入避免主线程lockCanvas()阻塞。但需手动管理SurfaceHolder.Callback生命周期这点后面避坑章节会深挖。2.2 参数模型必须用Immutable类而非一堆float字段很多项目用float brightness, float contrast, float saturation...散列字段管理状态后果是多线程修改时需synchronized锁整个View拖动滑块瞬间卡死无法做撤销/重做undo/redo因为没有状态快照导出配置时要手动拼JSON字段名硬编码易错。正确姿势定义不可变参数类ColorAdjustment所有调色操作都生成新实例// ColorAdjustment.java public final class ColorAdjustment { public final float brightness; // [-100, 100] public final float contrast; // [0.1f, 3.0f] public final float saturation; // [0.0f, 2.0f] public final float hue; // [-180, 180] 度 public final float temperature; // [-100, 100] 色温偏移 public final float tint; // [-100, 100] 绿品偏移 private ColorAdjustment(Builder builder) { this.brightness clamp(builder.brightness, -100, 100); this.contrast clamp(builder.contrast, 0.1f, 3.0f); this.saturation clamp(builder.saturation, 0.0f, 2.0f); this.hue clamp(builder.hue, -180, 180); this.temperature clamp(builder.temperature, -100, 100); this.tint clamp(builder.tint, -100, 100); } public static Builder newBuilder() { return new Builder(); } public static final class Builder { private float brightness 0; private float contrast 1.0f; private float saturation 1.0f; private float hue 0; private float temperature 0; private float tint 0; public Builder brightness(float v) { this.brightness v; return this; } public Builder contrast(float v) { this.contrast v; return this; } // ... 其他setter public ColorAdjustment build() { return new ColorAdjustment(this); } } private static float clamp(float value, float min, float max) { return Math.max(min, Math.min(max, value)); } }为什么有效ColorAdjustment无setter线程安全每次拖动滑块controlLayer生成新ColorAdjustment实例通过previewView.updateAdjustment(newAdjustment)传递previewView内部用AtomicReferenceColorAdjustment存储当前状态updateAdjustment()方法内仅替换引用毫秒级完成撤销栈只需存ListColorAdjustmentundo()就是adjustments.get(--index)。2.3 色相环Hue Wheel的触摸坐标到角度映射别信atan2(y,x)要校准中心偏移PhotoShop色相环是圆形用户手指在圆内滑动时需将(x,y)坐标转为[0,360)度角。看似Math.toDegrees(Math.atan2(y - centerY, x - centerX))就行但实际有3个坑屏幕坐标系Y轴向下而数学极坐标Y轴向上atan2参数顺序易反View可能因padding/margin导致getLeft()/getTop()与实际绘制区域中心不重合手指按压点不在圆心时初始角度需归零否则一碰就跳变。可靠方案在ColorControlLayer.onTouchEvent()中先计算触摸点相对于色相环绘制矩形的相对坐标再用atan2并手动归一化// ColorControlLayer.java private RectF hueWheelBounds new RectF(); // 色相环绘制区域 private float lastHueAngle 0f; Override public boolean onTouchEvent(MotionEvent event) { float x event.getX(); float y event.getY(); // 1. 判断是否在色相环区域内 if (!hueWheelBounds.contains(x, y)) return false; // 2. 计算相对于圆心的向量 float centerX hueWheelBounds.centerX(); float centerY hueWheelBounds.centerY(); float dx x - centerX; float dy centerY - y; // 注意y轴翻转屏幕y向下数学y向上 // 3. atan2返回[-π, π]转为[0, 2π)再转为[0,360) double angleRad Math.atan2(dy, dx); if (angleRad 0) angleRad 2 * Math.PI; float angleDeg (float) Math.toDegrees(angleRad); // 4. 首次按下时记录初始角度后续滑动以之为基准防跳变 if (event.getAction() MotionEvent.ACTION_DOWN) { lastHueAngle angleDeg; // 存储初始偏移用于ACTION_MOVE时计算delta initialHueOffset angleDeg; } else if (event.getAction() MotionEvent.ACTION_MOVE) { // 计算角度差避免跨0度时突变如从359°滑到1°应为2°而非-358° float delta angleDeg - lastHueAngle; if (delta 180) delta - 360; if (delta -180) delta 360; float newHue currentAdjustment.hue delta; notifyAdjustmentChange(ColorAdjustment.newBuilder() .copyFrom(currentAdjustment) .hue(newHue) .build()); lastHueAngle angleDeg; } return true; }关键细节说明dy centerY - y是核心修正不加此行色相环会镜像翻转initialHueOffset用于首次按下时锁定基准否则手指刚触屏就触发大角度跳变delta校准逻辑if (delta 180) delta - 360解决跨0度边界问题这是PhotoShop级体验的分水岭。3. 图像实时渲染引擎ColorMatrix vs RenderScript vs GPU Shader选型实战3.1 为什么ColorMatrix是入门首选但必须知道它的3个硬伤ColorMatrix是Android SDK内置的CPU图像处理工具用法简单// 在ColorPreviewView.draw()中 ColorMatrix matrix new ColorMatrix(); matrix.setSaturation(saturation); // 0灰度1原色2增强 matrix.postScale(1f, 1f, 1f, brightness / 100f 1f); // 亮度 // ... 设置对比度、色相等 Paint paint new Paint(); paint.setColorFilter(new ColorMatrixColorFilter(matrix)); canvas.drawBitmap(sourceBitmap, null, dstRect, paint);但它有3个致命缺陷必须提前认知不支持色相旋转Hue ShiftColorMatrix只能做线性变换缩放、平移而色相是极坐标旋转需用Rotate矩阵配合HSV转RGBSDK无现成API精度丢失严重ColorMatrix内部用float[20]数组但Android 7.0以下版本对负数处理有bugbrightness-50可能导致绿色通道溢出无法并行加速纯CPU执行1080p图单帧处理常超16ms60fps阈值低端机必掉帧。适用场景仅用于原型验证、小图480p快速调试、或作为降级兜底方案。生产环境必须升级。3.2 RenderScript在Android 5.0上跑出稳定60fps的秘诀RenderScript是Android官方推荐的高性能计算框架专为图像处理优化。它天然支持GPU加速当设备支持时且API比OpenGL ES友好得多。第一步创建RenderScript实例与Allocation// ColorPreviewView.java private RenderScript rs; private ScriptC_color_adjust script; private Allocation inAllocation; private Allocation outAllocation; private void initRenderScript() { rs RenderScript.create(getContext()); script new ScriptC_color_adjust(rs); // 自定义脚本见下文 // 创建输入输出Allocation内存分配 Bitmap.Config config sourceBitmap.getConfig() ! null ? sourceBitmap.getConfig() : Bitmap.Config.ARGB_8888; Type.Builder typeBuilder new Type.Builder(rs, Element.RGBA_8888(rs)); typeBuilder.setX(sourceBitmap.getWidth()).setY(sourceBitmap.getHeight()); inAllocation Allocation.createTyped(rs, typeBuilder.create(), Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); outAllocation Allocation.createTyped(rs, typeBuilder.create()); // 将Bitmap数据拷贝到inAllocation inAllocation.copyFrom(sourceBitmap); }第二步编写RenderScript内核color_adjust.rs// color_adjust.rs #pragma version(1) #pragma rs java_package_name(com.example.colorpalette) // 全局变量由Java端传入 float3 brightness {0.0f, 0.0f, 0.0f}; float contrast 1.0f; float saturation 1.0f; float hue 0.0f; // degree float temperature 0.0f; float tint 0.0f; // HSV转RGB辅助函数标准算法 static float3 hsv2rgb(float3 c) { float3 rgb; float h c.x, s c.y, v c.z; float i floor(h * 6.0f); float f h * 6.0f - i; float p v * (1.0f - s); float q v * (1.0f - f * s); float t v * (1.0f - (1.0f - f) * s); if (i 0.0f) rgb (float3){v, t, p}; else if (i 1.0f) rgb (float3){q, v, p}; else if (i 2.0f) rgb (float3){p, v, t}; else if (i 3.0f) rgb (float3){p, q, v}; else if (i 4.0f) rgb (float3){t, p, v}; else rgb (float3){v, p, q}; return rgb; } // RGB转HSV辅助函数 static float3 rgb2hsv(float3 c) { float r c.x, g c.y, b c.z; float min fmin(r, fmin(g, b)); float max fmax(r, fmax(g, b)); float delta max - min; float3 hsv; hsv.y max ! 0.0f ? delta / max : 0.0f; // S hsv.z max; // V if (max min) { hsv.x 0.0f; // H } else if (r max) { hsv.x (g - b) / delta (g b ? 6.0f : 0.0f); } else if (g max) { hsv.x (b - r) / delta 2.0f; } else { hsv.x (r - g) / delta 4.0f; } hsv.x / 6.0f; // 归一化到[0,1) return hsv; } // 主处理函数每个像素调用一次 void __attribute__((kernel)) root(const uchar4 *in, uchar4 *out) { float4 f4 rsUnpackFloat4(*in); float3 rgb (float3){f4.r, f4.g, f4.b}; // 1. RGB - HSV float3 hsv rgb2hsv(rgb); // 2. 应用色相旋转核心ColorMatrix做不到 hsv.x hue / 360.0f; // 转为[0,1)区间 hsv.x fmod(hsv.x 1.0f, 1.0f); // 处理越界 // 3. 应用饱和度 hsv.y saturate(hsv.y * saturation); // 4. 应用色温/色调简化模型在HSV空间微调 if (temperature ! 0.0f) { // 温度值偏黄-值偏蓝 → 调整H和S hsv.x temperature * 0.005f; hsv.y * (1.0f temperature * 0.002f); } if (tint ! 0.0f) { // 色调值偏绿-值偏品红 → 调整H hsv.x tint * 0.003f; } // 5. HSV - RGB rgb hsv2rgb(hsv); // 6. 应用亮度、对比度在RGB空间 rgb brightness; rgb (rgb - 0.5f) * contrast 0.5f; // 7. 输出 *out rsPackColorTo8888(rgb.r, rgb.g, rgb.b, f4.a); }第三步Java端调用脚本并同步结果// ColorPreviewView.java public void updateAdjustment(ColorAdjustment adj) { // 更新脚本全局变量 script.set_brightness(new Float3(adj.brightness/100f, adj.brightness/100f, adj.brightness/100f)); script.set_contrast(adj.contrast); script.set_saturation(adj.saturation); script.set_hue(adj.hue); script.set_temperature(adj.temperature); script.set_tint(adj.tint); // 执行脚本inAllocation - outAllocation script.forEach_root(inAllocation, outAllocation); // 同步结果回Bitmap注意必须在主线程调用 Bitmap result Bitmap.createBitmap(sourceBitmap.getWidth(), sourceBitmap.getHeight(), sourceBitmap.getConfig()); outAllocation.copyTo(result); // 更新预览 previewBitmap result; invalidate(); // 触发onDraw() }为什么RenderScript能稳60fpsforEach_root()自动并行化GPU核心数决定线程数Allocation内存由系统统一管理避免Java层Bitmap频繁GCrsUnpackFloat4/rsPackColorTo8888是硬件优化指令比手动位运算快3倍以上。血泪经验RenderScript在Android 5.0稳定但必须在onDetachedFromWindow()中调用rs.destroy()否则内存泄漏。我曾在线上版本漏掉这句导致用户连续编辑10张图后OOM。3.3 GPU Shader进阶当RenderScript也扛不住时用OpenGL ES 3.0写Fragment Shader当你的App需支持4K视频实时调色或要接入第三方美颜SDK如某主流美颜库要求GL纹理输入RenderScript的Allocation模型就不够用了。此时必须切到OpenGL ES 3.0用Fragment Shader直接操作纹理。核心流程创建GLSurfaceView实现GLSurfaceView.Renderer加载顶点/片元Shader编译链接成Program将原始Bitmap转为GL_TEXTURE_2D绑定到Sampler2D在onDrawFrame()中将ColorAdjustment参数传入Shader Uniform绘制全屏四边形QuadShader对每个像素采样并计算。片元Shader关键代码color_adjust.frag#version 300 es precision highp float; in vec2 v_TexCoord; out vec4 fragColor; uniform sampler2D s_Texture; uniform vec3 u_Brightness; // r,g,b分量 uniform float u_Contrast; uniform float u_Saturation; uniform float u_Hue; // degree uniform float u_Temperature; uniform float u_Tint; // HSV转换函数同RenderScript版此处省略 void main() { vec4 color texture(s_Texture, v_TexCoord); vec3 rgb color.rgb; // RGB - HSV vec3 hsv rgb2hsv(rgb); // 色相旋转 hsv.x u_Hue / 360.0; hsv.x mod(hsv.x 1.0, 1.0); // 饱和度 hsv.y clamp(hsv.y * u_Saturation, 0.0, 1.0); // 色温/色调同RenderScript逻辑 if (u_Temperature ! 0.0) { hsv.x u_Temperature * 0.005; hsv.y * (1.0 u_Temperature * 0.002); } if (u_Tint ! 0.0) { hsv.x u_Tint * 0.003; } // HSV - RGB rgb hsv2rgb(hsv); // 亮度、对比度 rgb u_Brightness; rgb (rgb - 0.5) * u_Contrast 0.5; fragColor vec4(rgb, color.a); }Java端传参示例// OpenGLRenderer.java private int mBrightnessLoc; private int mContrastLoc; private int mSaturationLoc; private int mHueLoc; private int mTemperatureLoc; private int mTintLoc; Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // 编译Shader获取Uniform位置 mProgram createProgram(vertexShader, fragmentShader); mBrightnessLoc GLES30.glGetUniformLocation(mProgram, u_Brightness); mContrastLoc GLES30.glGetUniformLocation(mProgram, u_Contrast); // ... 其他Uniform } Override public void onDrawFrame(GL10 gl) { GLES30.glUseProgram(mProgram); // 传入参数注意u_Brightness是vec3需传3个float GLES30.glUniform3f(mBrightnessLoc, adj.brightness/100f, adj.brightness/100f, adj.brightness/100f); GLES30.glUniform1f(mContrastLoc, adj.contrast); GLES30.glUniform1f(mSaturationLoc, adj.saturation); GLES30.glUniform1f(mHueLoc, adj.hue); GLES30.glUniform1f(mTemperatureLoc, adj.temperature); GLES30.glUniform1f(mTintLoc, adj.tint); // 绑定纹理、绘制 GLES30.glActiveTexture(GLES30.GL_TEXTURE0); GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTextureId); GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4); }何时必须上GPU Shader输入源是SurfaceTexture如CameraX预览流需叠加多层滤镜如先锐化再调色最后加光晕RenderScript难以链式调用目标设备明确支持OpenGL ES 3.0市占率95%可放心用。警告OpenGL ES开发调试成本高。glGetError()必须每帧检查否则黑屏无声。我建议先用RenderScript跑通全部逻辑再用GPU Shader替换性能瓶颈模块而非一上来就啃OpenGL。4. 避坑指南那些让调色器“看起来像PhotoShop”却“用起来像PPT”的5个致命细节4.1 现象滑块拖动时预览图闪烁尤其在低端机上原因Bitmap复用不当导致GC抖动解决用BitmapPool预分配inMutabletrue现象还原用户拖动饱和度滑块预览图每0.5秒闪一下黑边Logcat刷GC freed 12MB。根本原因每次updateAdjustment()都调用Bitmap.createBitmap()新建Bitmap而Android 7.0以下Bitmap内存不在Java堆但创建/销毁仍触发底层内存管理频繁GC导致SurfaceView缓冲区断连。解决方案预分配Bitmap池复用同一块内存// ColorPreviewView.java private LruCacheString, Bitmap bitmapPool; private Bitmap previewBitmap; // 指向池中某Bitmap private void initBitmapPool() { final int maxMemory (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize maxMemory / 8; // 取1/8作缓存 bitmapPool new LruCacheString, Bitmap(cacheSize) { Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount() / 1024; } }; } private Bitmap getOrCreateBitmap(int width, int height, Bitmap.Config config) { String key width x height _ config.name(); Bitmap bitmap bitmapPool.get(key); if (bitmap null) { bitmap Bitmap.createBitmap(width, height, config); bitmapPool.put(key, bitmap); } return bitmap; } // 在updateAdjustment()中 Bitmap result getOrCreateBitmap(sourceBitmap.getWidth(), sourceBitmap.getHeight(), sourceBitmap.getConfig()); outAllocation.copyTo(result); previewBitmap result; // 复用不新建注意Bitmap.Config.ARGB_8888必须显式指定否则某些机型默认ARGB_4444已废弃导致颜色失真。4.2 现象色相环拖动时角度跳变从10°突然跳到350°原因未处理跨0度边界解决用模运算归一化delta现象还原手指从色相环右侧0°顺时针滑到左侧180°再继续滑到顶部270°当经过0°线时角度从359°跳回0°delta 0 - 359 -359导致色相疯狂反转。错误代码常见于StackOverflow答案float delta newAngle - oldAngle; // 未校准正确解法已在2.3节给出此处强调float delta newAngle - oldAngle; if (delta 180) delta - 360; // 359→1 变成 -2 if (delta -180) delta 360; // 1→359 变成 2原理色相是环形空间最短路径距离才是真实拖动量。Math.abs(delta)最小化即为正确方向。4.3 现象导出图片后用Photoshop打开发现HSL值与滑块位置不符原因sRGB与Adobe RGB色彩空间混淆解决强制使用sRGB并禁用ICC配置文件现象还原App里把饱和度调到150%导出PNG用Photoshop的“信息”面板读取HSL显示饱和度仅120%。真相Android默认Bitmap是sRGB色彩空间但部分设备厂商尤其某国产旗舰在BitmapFactory.decodeResource()时注入了设备专属ICC配置文件导致颜色值被二次映射。铁律所有图像处理环节必须锁定sRGB创建Bitmap时指定Bitmap.Config.ARGB_8888隐含sRGBColorMatrix/RenderScript/Shader中所有计算基于sRGB伽马曲线无需额外gamma校正导出时禁用ICC// 导出PNG Bitmap result ...; FileOutputStream fos new FileOutputStream(file); result.compress(Bitmap.CompressFormat.PNG, 100, fos); fos.close(); // 关键不要用BitmapFactory.Options.inPreferredConfig它可能被系统篡改 // 不要调用bitmap.setHasAlpha(true)这会触发ICC重写提示用adb shell dumpsys SurfaceFlinger可查看当前Surface色彩空间确认是否为sRGB。4.4 现象开启硬件加速后ColorPreviewView预览图全黑原因SurfaceView与硬件加速冲突解决为SurfaceView关闭硬件加速现象还原AppApplication或Activity开启了android:hardwareAcceleratedtrueColorPreviewViewSurfaceView一片漆黑但Logcat无报错。原因SurfaceView的Surface是独立于View树的硬件加速的OpenGL上下文无法直接绘制到其Surface必须用SurfaceHolder的lockCanvas()软件绘制或eglMakeCurrent()手动EGL绑定。解决在ColorPreviewView构造函数中强制关闭硬件加速public ColorPreviewView(Context context, AttributeSet attrs) { super(context, attrs); // 关键SurfaceView必须关硬件加速 setLayerType(View.LAYER_TYPE_NONE, null); }补充若用TextureView替代SurfaceView则需开启硬件加速但TextureView的getBitmap()是主线程阻塞调用性能不如SurfaceView。权衡取舍。4.5 现象多指操作时第二个手指触发色相环误判原因未过滤非主指针事件解决用MotionEvent.getPointerId()锁定主指针现象还原用户左手按住亮度滑块右手去拖色相环色相环突然跳到随机角度。原因onTouchEvent()收到的是多指事件event.getX()/getY()默认返回第一个指针Index 0坐标但当左手先按下滑块右手再触屏时右手成为Index 0坐标被误认为色相环操作。正确做法记录首个按下的指针ID在ACTION_MOVE中只处理该IDprivate int primaryPointerId -1; Override public boolean onTouchEvent(MotionEvent event) { int action event.getActionMasked(); int pointerIndex event.getActionIndex(); if (action MotionEvent.ACTION_DOWN || action MotionEvent.ACTION_POINTER_DOWN) { if (primaryPointerId -1) { primaryPointerId event.getPointerId(pointerIndex); } } else if (action MotionEvent.ACTION_MOVE) { // 只处理主指针 int targetIndex event.findPointerIndex(primaryPointerId); if (targetIndex -1) return false; float x event.getX(targetIndex); float y event.getY(targetIndex); // ... 处理x,y } else if (action MotionEvent.ACTION_UP || action MotionEvent.ACTION_POINTER_UP) { if (event.getPointerId(pointerIndex) primaryPointerId) { primaryPointerId -1; } } return true; }5. 调色参数持久化与跨设备一致性如何让一张图在华为Mate60和小米Redmi Note上颜色完全一致5.1 为什么SharedPreferences存float不靠谱用Protocol Buffers序列化ColorAdjustment很多项目把brightness20.5f直接存SharedPreferences上线后发现Android 12设备读取的20.5f在Android 8.0上变成20.499998f多次保存加载后漂移float二进制表示在不同CPU架构ARMv7 vs ARM64有微小差异无法扩展新参数如新增vignetteIntensity旧版本App读取新配置会崩溃。终极方案用Protocol Buffers定义.proto文件生成Java类序列化为字节数组存储// color_adjustment.proto syntax proto3; message ColorAdjustment { float brightness 1; // [-100, 100] float contrast 2; // [0.1, 3.0] float saturation 3; // [0.0, 2.0] float hue 4; // [-180, 180] float temperature 5; // [-100, 100] float tint 6; // [-100, 100] int64 timestamp 7; // 保存时间用于版本追踪 }生成Java类后存取代码// 保存 ColorAdjustment proto ColorAdjustment.newBuilder() .setBrightness(adj.brightness) .setContrast(adj.contrast) .setSaturation(adj.saturation) .setHue(adj.hue) .setTemperature(adj.temperature) .setTint(adj.tint) .setTimestamp(System.currentTimeMillis()) .build(); byte[] data proto.toByteArray(); // 存入SharedPreferenceskey为color_config_v1 prefs.edit().putString(color_config_v1, Base64.encodeToString(data, Base64.NO_WRAP)).apply(); // 读取 String encoded prefs.getString(color_config_v1, ); if (!encoded.isEmpty()) { byte[] decoded Base64.decode(encoded, Base64.NO_WRAP); ColorAdjustment loaded ColorAdjustment.parseFrom(decoded); // 构建ColorAdjustment对象 ColorAdjustment adj ColorAdjustment.newBuilder() .brightness(loaded.getBrightness()) .contrast(loaded.getContrast()) p a hrefhttps://download.csdn.net/download/t12x3456/6246509 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
阅读完成 · 觉得有帮助?