在设计开发过程中,从图片中提取精确颜色值是一个常见需求。本文介绍如何在HarmonyOS中开发一款功能完整的图片取色器,支持:
通过PhotoViewPicker选择系统图片,使用ImageKit创建PixelMap获取像素数据:
async selectImage() {
const PhotoSelectOptions = new picker.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
const photoPicker = new picker.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then((result) => {
const file = fs.openSync(result.photoUris[0], fs.OpenMode.READ_ONLY);
const imageSource = image.createImageSource(file.fd);
const decodingOptions: image.DecodingOptions = {
editable: true,
desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
};
imageSource.createPixelMap(decodingOptions, (err, pixelMap) => {
if (!err) {
this.imagePixelMap = pixelMap;
const { width, height } = pixelMap.getImageInfoSync().size;
this.imageWidth = width;
this.imageHeight = height;
}
});
});
}
通过比例换算实现显示坐标与原始像素坐标的双向转换:
convertToImageCoordinates(displayX: number, displayY: number) {
return {
x: Math.floor((displayX - this.imageOffsetX) *
(this.imageWidth / this.imageDisplayWidth)),
y: Math.floor((displayY - this.imageOffsetY) *
(this.imageHeight / this.imageDisplayHeight))
};
}
像素数据读取采用RGBA_8888格式,高效准确:
getColorAtPosition(x: number, y: number) {
const area: image.PositionArea = {
pixels: new ArrayBuffer(4),
region: { size: { height: 1, width: 1 }, x, y }
};
this.imagePixelMap.readPixels(area).then(() => {
const [r, g, b] = new Uint8Array(area.pixels);
this.selectedColor = `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`;
this.rgbColor = `RGB(${r}, ${g}, ${b})`;
});
}
使用PanGesture实现流畅的十字准星拖动体验:
.gesture(
PanGesture()
.onActionStart(() => this.getColorAtPosition(...))
.onActionUpdate((event) => {
const clamped = this.clampCoordinates(...);
this.crosshairX = clamped.x;
this.crosshairY = clamped.y;
this.getColorAtPosition(...);
})
)
通过组合布局实现专业取色界面:
Stack() {
Image(this.imagePixelMap)
.objectFit(ImageFit.Contain)
.onAreaChange((_, newArea) =>
this.calculateImageDisplaySize(newArea.width, newArea.height))
// 十字准星组件
Column() {
Row() {
Divider().width(100) // 横线
Divider().height(100) // 竖线
}
}.position({x: this.crosshairX, y: this.crosshairY})
}
封装系统剪贴板操作:
function copyText(text: string) {
const pasteboardData = pasteboard.createData(
pasteboard.MIMETYPE_TEXT_PLAIN, text
);
pasteboard.getSystemPasteboard().setData(pasteboardData);
}
图片适配策略
通过objectFit: ImageFit.Contain
+坐标转换算法,完美解决不同分辨率图片的显示适配问题
性能优化实践
onAreaChange
替代全局重绘应用商店下载【图影工具箱】,点击取色器进入该功能。
(用户选择图片→移动十字准星→复制颜色值)
本文详细讲解了在HarmonyOS中开发图片取色器的完整流程,涵盖了从图片选择、像素解析到手势交互等关键技术的实现方案。开发者可基于此扩展出更专业的色彩工具,为HarmonyOS生态贡献更多实用的视觉开发工具。
完整代码:
import { image } from '@kit.ImageKit';
import { fileIo as fs, picker } from '@kit.CoreFileKit';
import { BusinessError, pasteboard } from '@kit.BasicServicesKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { promptAction } from '@kit.ArkUI';
import { TitleBar } from '../components/TitleBar';
interface GeneratedTypeLiteralInterface_1 {
x: number;
y: number;
}
interface GeneratedTypeLiteralInterface_2 {
x: number;
y: number;
}
@Extend(Row)
function settingsCard() {
.width('100%')
.padding(20)
.borderRadius(16)
.backgroundColor($r('sys.color.ohos_id_color_sub_background'))
.shadow({
radius: 8,
color: '#1A000000',
offsetX: 2,
offsetY: 4
})
}
// 定义方法
function copyText(text: string) {
// 创建剪贴板内容对象
const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
// 获取系统剪贴板对象
const systemPasteboard = pasteboard.getSystemPasteboard();
systemPasteboard.setData(pasteboardData); // 将数据放入剪贴板
systemPasteboard.getData().then((data) => { // 读取剪贴板内容
if (data) {
promptAction.showToast({ message: '复制成功' });
} else {
promptAction.showToast({ message: '复制失败' });
}
})
}
@Entry
@Component
struct ColorPickerPage {
@State private imageWidth: number = 0;
@State private imageHeight: number = 0;
@State private crosshairX: number = 0;
@State private crosshairY: number = 0;
@State private lastCrosshairX: number = 0;
@State private lastCrosshairY: number = 0;
@State private selectedColor: string = '#FFFFFF';
@State private rgbColor: string = 'RGB(255, 255, 255)';
@State private imagePixelMap: PixelMap | null = null;
@State private imageDisplayWidth: number = 0;
@State private imageDisplayHeight: number = 0;
@State private imageOffsetX: number = 0;
@State private imageOffsetY: number = 0;
lineLength: number = 100;
pixelArray = new Uint8Array();
r: number = 0;
g: number = 0;
b: number = 0;
aboutToAppear() {
// 加载示例图片
// this.selectImage();
}
async selectImage() {
try {
let uris: Array = [];
let PhotoSelectOptions = new picker.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 1;
let photoPicker = new picker.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => {
uris = PhotoSelectResult.photoUris;
let file = fs.openSync(uris[0], fs.OpenMode.READ_ONLY);
console.info('file fd: ' + file.fd);
let buffer = new ArrayBuffer(4096);
let readLen = fs.readSync(file.fd, buffer);
console.info('readSync data to file succeed and buffer size is:' + readLen);
const imageSource: image.ImageSource = image.createImageSource(file.fd);
let decodingOptions: image.DecodingOptions = {
editable: true,
desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
}
imageSource.createPixelMap(decodingOptions, (err: BusinessError, pixelMap: image.PixelMap) => {
if (err !== undefined) {
console.error(`Failed to create pixelMap.code is ${err.code},message is ${err.message}`);
} else {
this.imagePixelMap = pixelMap;
// 获取图片原始尺寸
const imageInfo = pixelMap.getImageInfoSync();
this.imageWidth = imageInfo.size.width;
this.imageHeight = imageInfo.size.height;
// 重置十字架位置
this.crosshairX = 0;
this.crosshairY = 0;
this.lastCrosshairX = 0;
this.lastCrosshairY = 0;
console.info('Succeeded in creating pixelMap object.');
}
fs.closeSync(file);
})
}).catch((err: BusinessError) => {
console.error(`Invoke photoPicker.select failed, code is ${err.code}, message is ${err.message}`);
})
} catch (error) {
let err: BusinessError = error as BusinessError;
console.error('photoPicker failed with err: ' + JSON.stringify(err));
}
}
// 计算图片在容器中的实际显示尺寸和偏移量
calculateImageDisplaySize(containerWidth: number, containerHeight: number) {
const imageRatio = this.imageWidth / this.imageHeight;
const containerRatio = containerWidth / containerHeight;
if (imageRatio > containerRatio) {
// 图片更宽,以容器宽度为基准
this.imageDisplayWidth = containerWidth;
this.imageDisplayHeight = containerWidth / imageRatio;
this.imageOffsetX = 0;
this.imageOffsetY = (containerHeight - this.imageDisplayHeight) / 2;
} else {
// 图片更高,以容器高度为基准
this.imageDisplayHeight = containerHeight;
this.imageDisplayWidth = containerHeight * imageRatio;
this.imageOffsetX = (containerWidth - this.imageDisplayWidth) / 2;
this.imageOffsetY = 0;
}
// 初始化十字架位置到图片中心
this.crosshairX = this.imageOffsetX + this.imageDisplayWidth / 2;
this.crosshairY = this.imageOffsetY + this.imageDisplayHeight / 2;
this.lastCrosshairX = this.crosshairX;
this.lastCrosshairY = this.crosshairY;
}
// 将显示坐标转换为图片原始坐标
convertToImageCoordinates(displayX: number, displayY: number): GeneratedTypeLiteralInterface_1 {
const x = Math.floor((displayX - this.imageOffsetX) * (this.imageWidth / this.imageDisplayWidth));
const y = Math.floor((displayY - this.imageOffsetY) * (this.imageHeight / this.imageDisplayHeight));
return { x, y };
}
// 限制坐标在图片显示范围内
clampCoordinates(x: number, y: number): GeneratedTypeLiteralInterface_2 {
return {
x: Math.max(this.imageOffsetX, Math.min(x, this.imageOffsetX + this.imageDisplayWidth)),
y: Math.max(this.imageOffsetY, Math.min(y, this.imageOffsetY + this.imageDisplayHeight))
};
}
getColorAtPosition(x: number, y: number) {
if (!this.imagePixelMap) {
return;
}
try {
// 转换坐标
const imageCoords = this.convertToImageCoordinates(x, y);
// 确保坐标在图片范围内
if (imageCoords.x < 0 || imageCoords.x >= this.imageWidth ||
imageCoords.y < 0 || imageCoords.y >= this.imageHeight) {
return;
}
const area: image.PositionArea = {
pixels: new ArrayBuffer(4), // RGBA 格式,每个像素4字节
offset: 0,
stride: 4,
region: {
size: { height: 1, width: 1 },
x: imageCoords.x,
y: imageCoords.y
}
}
this.imagePixelMap.readPixels(area).then(() => {
this.pixelArray = new Uint8Array(area.pixels);
this.r = this.pixelArray[2];
this.g = this.pixelArray[1];
this.b = this.pixelArray[0];
console.log('xxx123 changeColor')
this.selectedColor =
`#${this.r.toString(16).padStart(2, '0')}${this.g.toString(16).padStart(2, '0')}${this.b.toString(16)
.padStart(2, '0')}`;
this.rgbColor = `RGB(${this.r}, ${this.g}, ${this.b})`;
}).catch((error: BusinessError) => {
console.error('Failed to read pixel data:', error);
});
} catch (error) {
console.error('Failed to read pixel data:', error);
}
}
build() {
Column() {
// 顶部标题区域
TitleBar({
title: '图片取色器'
})
if (!this.imagePixelMap) {
// 选择图片按钮
Button('选择图片')
.width('90%')
.height(50)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.borderRadius(25)
.margin({ top: 20, bottom: 10 })
.onClick(() => {
this.selectImage();
})
}
// 图片显示区域
Stack() {
if (this.imagePixelMap) {
Image(this.imagePixelMap)
.width('100%')
.height(400)
.objectFit(ImageFit.Contain)
.backgroundColor($r('app.color.index_tab_bar'))
.onAreaChange((oldValue: Area, newValue: Area) => {
this.calculateImageDisplaySize(newValue.width as number, newValue.height as number);
})
// 十字标线
Column() {
Row() {
// 横线
Divider()
.width(this.lineLength)
.height('2px')
.offset({ x: -this.lineLength / 2, y: -this.lineLength / 2 })
.backgroundColor($r('app.color.index_tab_bar'))
.shadow({
radius: 2,
color: '#40000000',
offsetX: 0,
offsetY: 1
})
// 竖线
Divider()
.width('2px')
.height(this.lineLength)
.offset({ x: -this.lineLength, y: -this.lineLength / 2 })
.backgroundColor($r('app.color.index_tab_bar'))
.shadow({
radius: 2,
color: '#40000000',
offsetX: 0,
offsetY: 1
})
}
}
.position({ x: this.crosshairX, y: this.crosshairY })
} else {
// 未选择图片时的提示
Column() {
Image($r('app.media.select_empty'))
.width(48)
.height(48)
.margin({ bottom: 16 })
Text('请选择一张图片')
.fontSize(18)
}
.width('100%')
.height(300)
.justifyContent(FlexAlign.Center)
.onClick(async () => {
await this.selectImage();
})
}
}.gesture(
PanGesture()
.onActionStart((event: GestureEvent) => {
this.crosshairX = this.lastCrosshairX;
this.crosshairY = this.lastCrosshairY;
this.getColorAtPosition(this.crosshairX, this.crosshairY);
})
.onActionUpdate((event: GestureEvent) => {
const clampedCoords = this.clampCoordinates(
this.lastCrosshairX + event.offsetX,
this.lastCrosshairY + event.offsetY
);
this.crosshairX = clampedCoords.x;
this.crosshairY = clampedCoords.y;
this.getColorAtPosition(this.crosshairX, this.crosshairY);
})
.onActionEnd((event: GestureEvent) => {
this.lastCrosshairX = this.crosshairX;
this.lastCrosshairY = this.crosshairY;
})
)
.width('100%')
.height(400)
.margin({ top: 10, bottom: 20 })
// 颜色信息显示区域
Column() {
// 颜色预览和颜色值
Row() {
// 颜色预览区域 - 固定宽度
Column() {
Text('颜色预览')
.fontSize(16)
.fontColor('#666666')
.margin({ bottom: 10 })
Column()
.width(60)
.height(60)
.backgroundColor(this.selectedColor)
.borderRadius(8)
.border({ width: 1, color: $r('app.color.border_color') })
}
.width(100) // 固定宽度
.alignItems(HorizontalAlign.Center)
.margin({ right: 20 })
// 颜色值区域 - 自适应宽度
Column() {
Text('颜色值')
.fontSize(16)
.fontColor('#666666')
.margin({ bottom: 10 })
// Hex颜色值
Row() {
Text(this.selectedColor)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
Button() {
Image($r('app.media.copy'))
.width(20)
.height(20)
}
.width(36)
.height(36)
.borderRadius(18)
.onClick(() => {
copyText(this.selectedColor);
})
}
.width('100%')
.margin({ bottom: 12 })
.padding({ left: 4, right: 4 })
.backgroundColor($r('app.color.index_tab_bar'))
.borderRadius(8)
.height(48)
.alignItems(VerticalAlign.Center)
// RGB颜色值
Row() {
Text(this.rgbColor)
.fontSize(16)
.layoutWeight(1)
Button() {
Image($r('app.media.copy'))
.width(20)
.height(20)
}
.width(36)
.height(36)
.borderRadius(18)
.onClick(() => {
copyText(this.rgbColor);
})
}
.width('100%')
.padding({ left: 4, right: 4 })
.backgroundColor($r('app.color.index_tab_bar'))
.borderRadius(8)
.height(48)
.alignItems(VerticalAlign.Center)
}
.layoutWeight(1) // 自适应宽度
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.settingsCard()
.justifyContent(FlexAlign.Start) // 改为左对齐
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 20 })
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.index_tab_bar'))
}
}