Реалізація обрізки і редактування фотографій у мобільному додатку
Користувач хоче обрізати аватарку в круг із соотношенням 1:1. Розробник бере UIImageView, додає жест pinch-to-zoom — і через день розуміє, що трансформації накопливаються некоректно, а при експорті зображення обрізується не по тому прямокутнику, що видить користувач.
Головна проблема: система координат при обрізці
Редактор відображає превью в imageView певного розміру, але вихідне зображення — 4000×3000 px. Прямокутник обрізки в координатах екрана потрібно пересчитати в координати вихідного зображення. Масштаб — imageView.bounds vs image.size, з урахуванням contentMode. UIImageView з aspectFit додає letterbox-відступи — їх надо відняти перед масштабуванням.
На Android та сама історія з Matrix і getImageMatrix() на ImageView.
Як будуємо редактор
iOS. Два варіанти: готова CropViewController з бібліотеки TOCropViewController або кастомна реалізація. Для більшості завдань TOCropViewController закриває 90% вимог — соотношення сторін, вращение, кругла маска. Якщо потрібен власний UI — рисуємо UIScrollView з UIImageView всередину, поверх — CAShapeLayer з вирізом. При фінальному експорті:
let cropRect = cropRectInImageCoordinates() // пересчёт з UI-координат
let cgImage = image.cgImage!.cropping(to: cropRect)
let result = UIImage(cgImage: cgImage!, scale: image.scale, orientation: image.imageOrientation)
Android. uCrop — стандарт де-факто. UCrop.of(sourceUri, destinationUri).withAspectRatio(1f, 1f).start(activity). Під капотом — OpenGL ES для плавного превью, фінальна обрізка через BitmapRegionDecoder для економії пам'яті при великих вихідниках.
Flutter. image_cropper (pub.dev) — обертає uCrop на Android і TOCropViewController на iOS. Кастомізація через CropStyle, CropAspectRatio. Для повністю нативного Flutter-рішення — пакет crop.
Яскравість, контраст, насиченість
Реалізуємо через GPU-шейдери. На iOS — CIFilter: CIColorControls (яскравість, контраст, насиченість), CIExposureAdjust, CIHueAdjust. Рендер через CIContext з kCIContextUseSoftwareRenderer: false — використовуємо GPU, уникаємо гальм.
На Android — ColorMatrix + ColorMatrixColorFilter для базових коррекцій, або RenderScript (застарів в API 31) → GPUImage (OpenGL ES). Для нових проектів — androidx.renderscript через renderscript-toolkit.
Превью робимо у реальному часі через debounce на ползунку (150 мс), щоб не перегружати GPU при швидкому русі слайдера.
Збереження результату
Експортуємо в JPEG (compressionQuality: 0.88 — баланс якості і розміру для стандартних аватарок). Для документів — PNG без втрат. Тимчасові файли пишемо в Caches, фінальний — в Documents або передаємо через FileProvider (Android).
Flutter: image_cropper vs кастомне рішення
image_cropper (pub.dev) викликає нативні uCrop/TOCropViewController. Плюс — надійність нативних компонентів. Мінус — UI не кастомізується глибоко, складно вписати в дизайн-систему додатку. Для повного контролю над UI використовуємо extended_image (pub.dev) — Flutter-нативний кроппер з підтримкою жестів pinch і rotate, рисує через CustomPaint.
Строки
Простий кроппер з фіксованим соотношенням і кнопками «Повернути» — 2 дні. Редактор з коррекцією яскравості/контрасту, кількома соотношеннями і превью у реальному часі — 3–4 дні.







