HarmonyOS NEXT Development Case Study: Jigsaw Puzzle
This article demonstrates a jigsaw puzzle implementation using HarmonyOS NEXT, featuring image selection, puzzle piece generation, shuffling, and game logic. The solution leverages ArkUI components and HarmonyOS native capabilities. Project Structure 1. Module Imports // Core modules import { fileIo as fs } from '@kit.CoreFileKit'; // File operations module import { common } from '@kit.AbilityKit'; // Application capability module import { promptAction } from '@kit.ArkUI'; // UI prompt module import { image } from '@kit.ImageKit'; // Image processing module import { photoAccessHelper } from '@kit.MediaLibraryKit'; // Media library access module 2. Data Structure interface PuzzlePiece { pixelMap: image.PixelMap; // Pixel map of the puzzle piece originalIndex: number; // Original position index in the image } Core Implementation 3. Page Component Setup @Entry // Entry component @Component // Custom component struct Page30 { @State selectedImageUrl: string = ''; // Displayed image URI @State originalImageUrl: string = ''; // Source image URI @State puzzlePieces: Array = []; // Puzzle pieces array @State selectedPiece: number = -1; // Selected piece index 4. Image Selection Logic async openPicker() { try { const PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; PhotoSelectOptions.maxSelectNumber = 1; const photoPicker = new photoAccessHelper.PhotoViewPicker(); const uris = await photoPicker.select(PhotoSelectOptions); if (!uris?.photoUris.length) return; const uri = uris.photoUris[0]; // File operations const file = fs.openSync(uri, fs.OpenMode.READ_ONLY); const context = getContext(this) as common.UIAbilityContext; const newUrl = `${context.filesDir}/test${Date.now()}.jpg`; fs.copyFileSync(file.fd, newUrl); fs.closeSync(file); this.selectedImageUrl = newUrl; this.originalImageUrl = uri; this.imgChange(); } catch (e) { console.error('openPicker', JSON.stringify(e)); } } 5. Image Processing async imgChange() { try { const imageSource = image.createImageSource(this.selectedImageUrl); const decodingOptions: image.DecodingOptions = { editable: true, desiredPixelFormat: 3 }; const mPixelMap = await imageSource.createPixelMap(decodingOptions); const mImageInfo = await mPixelMap.getImageInfo(); // Calculate grid dimensions const pieceSize: image.Size = { width: mImageInfo.size.width / 3, height: mImageInfo.size.height / 3 }; this.puzzlePieces = []; let count = 0; // Generate puzzle pieces for (let row = 0; row this.openPicker()); if (this.originalImageUrl) { Text('Original Image ↓'); Image(this.originalImageUrl) .size({ width: '180lpx', height: '180lpx' }) .objectFit(ImageFit.Contain); } if (this.puzzlePieces.length) { Text('Puzzle Game ↓'); Grid() { ForEach(this.puzzlePieces, (item, index) => { GridItem() { Image(item.pixelMap) .size({ width: '200lpx', height: '200lpx' }) .margin('5lpx') .scale(this.selectedPiece === index ? { x: 0.5, y: 0.5 } : { x: 1, y: 1 }) .onClick(() => { if (this.selectedPiece === -1) { this.selectedPiece = index; } else if (this.selectedPiece === index) { this.selectedPiece = -1; } else { // Swap pieces [this.puzzlePieces[this.selectedPiece], this.puzzlePieces[index]] = [this.puzzlePieces[index], this.puzzlePieces[this.selectedPiece]]; // Check completion const isComplete = this.puzzlePieces.every( (piece, i) => piece.originalIndex === i ); this.selectedPiece = -1; if (isComplete) { promptAction.showDialog({ message: 'Puzzle Completed!' }); } } }); } }) } .backgroundColor('#fafafa'); } .width('100%'); } Key Features Media Library Integration: Uses photoAccessHelper for secure image selection Implements proper file handling with sandboxed storage Image Processing: Dynamic grid division based on image dimensions Non-destructive editing using pixel map operations Game Mechanics: Fisher-Yates shuffle algorithm Visual selection feedback with scaling animation Real-time completion check Performance: Asynchronous operations for image processing Memory management through proper resource cleanup This implementation demonstrates HarmonyOS NEXT's capabilities in building interactive media applications w

This article demonstrates a jigsaw puzzle implementation using HarmonyOS NEXT, featuring image selection, puzzle piece generation, shuffling, and game logic. The solution leverages ArkUI components and HarmonyOS native capabilities.
Project Structure
1. Module Imports
// Core modules
import { fileIo as fs } from '@kit.CoreFileKit'; // File operations module
import { common } from '@kit.AbilityKit'; // Application capability module
import { promptAction } from '@kit.ArkUI'; // UI prompt module
import { image } from '@kit.ImageKit'; // Image processing module
import { photoAccessHelper } from '@kit.MediaLibraryKit'; // Media library access module
2. Data Structure
interface PuzzlePiece {
pixelMap: image.PixelMap; // Pixel map of the puzzle piece
originalIndex: number; // Original position index in the image
}
Core Implementation
3. Page Component Setup
@Entry // Entry component
@Component // Custom component
struct Page30 {
@State selectedImageUrl: string = ''; // Displayed image URI
@State originalImageUrl: string = ''; // Source image URI
@State puzzlePieces: Array<PuzzlePiece> = []; // Puzzle pieces array
@State selectedPiece: number = -1; // Selected piece index
4. Image Selection Logic
async openPicker() {
try {
const PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 1;
const photoPicker = new photoAccessHelper.PhotoViewPicker();
const uris = await photoPicker.select(PhotoSelectOptions);
if (!uris?.photoUris.length) return;
const uri = uris.photoUris[0];
// File operations
const file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
const context = getContext(this) as common.UIAbilityContext;
const newUrl = `${context.filesDir}/test${Date.now()}.jpg`;
fs.copyFileSync(file.fd, newUrl);
fs.closeSync(file);
this.selectedImageUrl = newUrl;
this.originalImageUrl = uri;
this.imgChange();
} catch (e) {
console.error('openPicker', JSON.stringify(e));
}
}
5. Image Processing
async imgChange() {
try {
const imageSource = image.createImageSource(this.selectedImageUrl);
const decodingOptions: image.DecodingOptions = {
editable: true,
desiredPixelFormat: 3
};
const mPixelMap = await imageSource.createPixelMap(decodingOptions);
const mImageInfo = await mPixelMap.getImageInfo();
// Calculate grid dimensions
const pieceSize: image.Size = {
width: mImageInfo.size.width / 3,
height: mImageInfo.size.height / 3
};
this.puzzlePieces = [];
let count = 0;
// Generate puzzle pieces
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 3; col++) {
const cutRegion: image.Region = {
x: col * pieceSize.width,
y: row * pieceSize.height,
size: pieceSize
};
const newSource = image.createImageSource(this.selectedImageUrl);
const pieceMap = await newSource.createPixelMap(decodingOptions);
await pieceMap.crop(cutRegion);
this.puzzlePieces.push({
pixelMap: pieceMap,
originalIndex: count++
});
}
}
// Shuffle algorithm
for (let i = this.puzzlePieces.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.puzzlePieces[i], this.puzzlePieces[j]] =
[this.puzzlePieces[j], this.puzzlePieces[i]];
}
} catch (e) {
console.error('imgChange', JSON.stringify(e));
}
}
UI Implementation
6. Game Interface
build() {
Column() {
Button('Select Image →')
.onClick(() => this.openPicker());
if (this.originalImageUrl) {
Text('Original Image ↓');
Image(this.originalImageUrl)
.size({ width: '180lpx', height: '180lpx' })
.objectFit(ImageFit.Contain);
}
if (this.puzzlePieces.length) {
Text('Puzzle Game ↓');
Grid() {
ForEach(this.puzzlePieces, (item, index) => {
GridItem() {
Image(item.pixelMap)
.size({ width: '200lpx', height: '200lpx' })
.margin('5lpx')
.scale(this.selectedPiece === index ? { x: 0.5, y: 0.5 } : { x: 1, y: 1 })
.onClick(() => {
if (this.selectedPiece === -1) {
this.selectedPiece = index;
} else if (this.selectedPiece === index) {
this.selectedPiece = -1;
} else {
// Swap pieces
[this.puzzlePieces[this.selectedPiece], this.puzzlePieces[index]] =
[this.puzzlePieces[index], this.puzzlePieces[this.selectedPiece]];
// Check completion
const isComplete = this.puzzlePieces.every(
(piece, i) => piece.originalIndex === i
);
this.selectedPiece = -1;
if (isComplete) {
promptAction.showDialog({ message: 'Puzzle Completed!' });
}
}
});
}
})
}
.backgroundColor('#fafafa');
}
.width('100%');
}
Key Features
-
Media Library Integration:
- Uses
photoAccessHelper
for secure image selection - Implements proper file handling with sandboxed storage
- Uses
-
Image Processing:
- Dynamic grid division based on image dimensions
- Non-destructive editing using pixel map operations
-
Game Mechanics:
- Fisher-Yates shuffle algorithm
- Visual selection feedback with scaling animation
- Real-time completion check
-
Performance:
- Asynchronous operations for image processing
- Memory management through proper resource cleanup
This implementation demonstrates HarmonyOS NEXT's capabilities in building interactive media applications with proper resource management and responsive UI design.