Skip to content

Commit

Permalink
Merge pull request #8 from tribalmedia/features/image_editor
Browse files Browse the repository at this point in the history
Features/image editor
  • Loading branch information
n-cong authored Apr 4, 2018
2 parents 5731c2e + 16aeb13 commit bbe29c4
Show file tree
Hide file tree
Showing 29 changed files with 720 additions and 270 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ class FMZoomInAnimationController: NSObject, UIViewControllerAnimatedTransitioni
let bgView = UIView(frame: containerView.frame)
containerView.addSubview(bgView)
containerView.addSubview(snapshot)
snapshot.frame = self.realDestinationFrame(scaledFrame: self.getOriginFrame(), realSize: snapshot.frame.size)

let originalSnapshotCornerRadius = snapshot.layer.cornerRadius
let originalSnapshotSize = snapshot.frame.size
let startFrame = self.realDestinationFrame(scaledFrame: self.getOriginFrame(), realSize: snapshot.frame.size)

snapshot.layer.cornerRadius = 0
snapshot.frame = startFrame

containerView.addSubview(toVC.view)
containerView.addSubview(snapshot)
Expand All @@ -49,8 +55,9 @@ class FMZoomInAnimationController: NSObject, UIViewControllerAnimatedTransitioni
}

UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.9) {
snapshot.frame = CGRect(x: 0, y: 0, width: photoVC.viewToSnapshot().frame.width, height: photoVC.viewToSnapshot().frame.height)
snapshot.frame = CGRect(origin: .zero, size: originalSnapshotSize)
snapshot.center = containerView.center
snapshot.layer.cornerRadius = originalSnapshotCornerRadius
}

UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,24 @@ class FMZoomOutAnimationController: NSObject, UIViewControllerAnimatedTransition
return
}

let snapshot = photoVC.viewToSnapshot().ins_snapshotView()
let containerView = transitionContext.containerView

let pannedVector = fromVC.pageViewController.view.frame.origin

let snapshot = photoVC.viewToSnapshot().ins_snapshotView()
containerView.addSubview(snapshot)
snapshot.frame = CGRect(x: 0, y: 0, width: photoVC.viewToSnapshot().frame.width, height: photoVC.viewToSnapshot().frame.height)
snapshot.center = containerView.center

let originSnapshotSize = snapshot.frame.size

snapshot.frame = CGRect(origin: .zero, size: originSnapshotSize)
snapshot.center = containerView.center

snapshot.frame = CGRect(origin: CGPoint(x: snapshot.frame.origin.x + pannedVector.x,
y: snapshot.frame.origin.y + pannedVector.y),
size: snapshot.frame.size)
size: originSnapshotSize)

fromVC.view.isHidden = true

let duration = transitionDuration(using: transitionContext)

UIView.animateKeyframes(
Expand All @@ -52,6 +58,7 @@ class FMZoomOutAnimationController: NSObject, UIViewControllerAnimatedTransition
animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.9) {
snapshot.frame = self.realDestinationFrame(scaledFrame: self.getDestFrame(), realSize: snapshot.frame.size)
snapshot.layer.cornerRadius = 0
}

UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1) {
Expand Down
144 changes: 100 additions & 44 deletions FMPhotoPicker/FMPhotoPicker/source/Data/FMPhotoAsset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,77 +10,142 @@ import Foundation
import Photos

public class FMPhotoAsset {
var asset: PHAsset
let asset: PHAsset?
let sourceImage: UIImage?

var mediaType: FMMediaType
var thumb: UIImage?
var thumbRequestId: PHImageRequestID?

var originalThumb: UIImage?
// a fully edited thumbnail version of the image
var editedThumb: UIImage?

// a filterd-only thumbnail version of the image
var filterdThumb: UIImage?

var thumbRequestId: PHImageRequestID?

var videoFrames: [CGImage]?

var thumbChanged: (UIImage) -> Void = { _ in }

private var fullSizePhotoRequestId: PHImageRequestID?
private var editor = FMImageEditor()
private var editor: FMImageEditor!
/**
Indicates whether the request for the full size image was canceled.
A workaround for this issue:
https://stackoverflow.com/questions/48657304/phimagemanagers-cancelimagerequest-does-not-work-as-expected?noredirect=1#comment84332723_48657304
*/
private var canceledFullSizeRequest = false

init(asset: PHAsset) {
init(asset: PHAsset, forceCropType: FMCroppable?) {
self.asset = asset
self.mediaType = FMMediaType(withPHAssetMediaType: asset.mediaType)
self.sourceImage = nil

self.editor = self.initializeEditor(for: forceCropType, imageSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight))
}

init(sourceImage: UIImage, forceCropType: FMCroppable?) {
self.sourceImage = sourceImage
self.mediaType = .image
self.asset = nil

self.editor = self.initializeEditor(for: forceCropType, imageSize: sourceImage.size)
}

private func initializeEditor(for forceCropType: FMCroppable?, imageSize: CGSize) -> FMImageEditor {
guard let forceCropType = forceCropType, let fmCropRatio = forceCropType.ratio() else {
return FMImageEditor()
}

let imageRatio = CGFloat(imageSize.width) / CGFloat(imageSize.height)
let cropRatio = fmCropRatio.width / fmCropRatio.height
var scaleW, scaleH: CGFloat
if imageRatio > cropRatio {
scaleH = 1.0
scaleW = cropRatio / imageRatio
} else {
scaleW = 1.0
scaleH = imageRatio / cropRatio
}
let cropArea = FMCropArea(scaleX: (1 - scaleW) / 2,
scaleY: (1 - scaleH) / 2,
scaleW: scaleW,
scaleH: scaleH)
return FMImageEditor(filter: kDefaultFilter, crop: forceCropType, cropArea: cropArea)
}

func requestVideoFrames(_ complete: @escaping ([CGImage]) -> Void) {
if let videoFrames = self.videoFrames {
complete(videoFrames)
} else {
Helper.generateVideoFrames(from: self.asset) { cgImages in
self.videoFrames = cgImages
complete(cgImages)
if let asset = asset {
Helper.generateVideoFrames(from: asset) { cgImages in
self.videoFrames = cgImages
complete(cgImages)
}
} else {
complete([])
}
}
}

func requestThumb(_ complete: @escaping (UIImage?) -> Void) {
if let thumb = self.thumb {
complete(thumb)
func requestThumb(refresh: Bool=false, _ complete: @escaping (UIImage?) -> Void) {
if let editedThumb = self.editedThumb, !refresh {
complete(editedThumb)
} else {
self.thumbRequestId = Helper.getPhoto(by: self.asset, in: CGSize(width: 150, height: 150)) { image in
self.thumbRequestId = nil
self.thumb = image
self.originalThumb = image

guard let image = image else { return complete(nil) }
let edited = self.editor.reproduce(source: image, cropState: .edited, filterState: .edited)
// It is not absolutely right but it gives much better performance in most cases
let cropScale = (editor.cropArea.scaleW + editor.cropArea.scaleH) / 2
let size = CGSize(width: 200 / cropScale, height: 200 / cropScale)
if let asset = asset {
self.thumbRequestId = Helper.getPhoto(by: asset, in: size) { image in
self.thumbRequestId = nil

guard let image = image else { return complete(nil) }

self.editedThumb = self.editor.reproduce(source: image, cropState: .edited, filterState: .edited)
self.filterdThumb = self.editor.reproduce(source: image, cropState: .edited, filterState: .original)

complete(self.editedThumb)
}
} else {
guard let image = sourceImage else { return complete(nil) }
let edited = self.editor.reproduce(source: image.resize(toSizeInPixel: size), cropState: .edited, filterState: .edited)
complete(edited)
}
}
}

func requestImage(in desireSize: CGSize, _ complete: @escaping (UIImage?) -> Void) {
_ = Helper.getPhoto(by: self.asset, in: desireSize) { image in
guard let image = image else { return complete(nil) }
if let asset = asset {
_ = Helper.getPhoto(by: asset, in: desireSize) { image in
guard let image = image else { return complete(nil) }
let edited = self.editor.reproduce(source: image, cropState: .edited, filterState: .edited)
complete(edited)
}
} else {
guard let image = sourceImage?.resize(toSizeInPixel: desireSize) else { return complete(nil) }
let edited = self.editor.reproduce(source: image, cropState: .edited, filterState: .edited)
complete(edited)
}
}

func requestFullSizePhoto(cropState: FMImageEditState, filterState: FMImageEditState, complete: @escaping (UIImage?) -> Void) {
self.fullSizePhotoRequestId = Helper.getFullSizePhoto(by: self.asset) { image in
self.fullSizePhotoRequestId = nil
if self.canceledFullSizeRequest {
self.canceledFullSizeRequest = false
complete(nil)
} else {
guard let image = image else { return complete(nil) }
let result = self.editor.reproduce(source: image, cropState: cropState, filterState: filterState)
complete(result)
if let asset = asset {
self.fullSizePhotoRequestId = Helper.getFullSizePhoto(by: asset) { image in
self.fullSizePhotoRequestId = nil
if self.canceledFullSizeRequest {
self.canceledFullSizeRequest = false
complete(nil)
} else {
guard let image = image else { return complete(nil) }
let result = self.editor.reproduce(source: image, cropState: cropState, filterState: filterState)
complete(result)
}
}
} else {
guard let image = sourceImage else { return complete(nil) }
let result = self.editor.reproduce(source: image, cropState: cropState, filterState: filterState)
complete(result)
}
}

Expand Down Expand Up @@ -114,28 +179,19 @@ public class FMPhotoAsset {
return editor.cropArea
}

public func getAppliedZoomScale() -> CGFloat? {
return editor.zoomScale
}

public func apply(filter: FMFilterable, crop: FMCroppable, cropArea: FMCropArea, zoomScale: CGFloat) {
public func apply(filter: FMFilterable, crop: FMCroppable, cropArea: FMCropArea) {
editor.filter = filter
editor.crop = crop
editor.cropArea = cropArea
editor.zoomScale = zoomScale

if let source = originalThumb {
thumb = editor.reproduce(source: source, cropState: .edited, filterState: .edited)
if thumb != nil {
thumbChanged(thumb!)
requestThumb(refresh: true) { image in
if let image = image {
self.thumbChanged(image)
}
}
}

public func isEdited() -> Bool {
if editor.filter as? FMFilter != FMFilter.None { return true }
if !editor.cropArea.isApproximatelyEqualToOriginal() { return true }

return false
return editor.isEdited()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ class FMPhotosDataSource {
return self.selectedPhotoIndexes.count
}

public func mediaTypeForPhoto(atIndex index: Int) -> PHAssetMediaType? {
return self.photo(atIndex: index)?.asset.mediaType
public func mediaTypeForPhoto(atIndex index: Int) -> FMMediaType? {
return self.photo(atIndex: index)?.mediaType
}

public func countSelectedPhoto(byType: PHAssetMediaType) -> Int {
return self.getSelectedPhotos().filter { $0.asset.mediaType == byType }.count
public func countSelectedPhoto(byType: FMMediaType) -> Int {
return self.getSelectedPhotos().filter { $0.mediaType == byType }.count
}

public func affectedSelectedIndexs(changedIndex: Int) -> [Int] {
Expand Down
19 changes: 11 additions & 8 deletions FMPhotoPicker/FMPhotoPicker/source/ImageEditor/Crop/FMCrop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,25 @@ public enum FMCrop: FMCroppable {

switch orientation {
case .down:
targetRect.origin.x = image.size.width - rect.maxX * scale
targetRect.origin.y = image.size.height - rect.maxY * scale
targetRect.origin.x = (image.size.width - rect.maxX) * scale
targetRect.origin.y = (image.size.height - rect.maxY) * scale
targetRect.size.width = rect.width * scale
targetRect.size.height = rect.height * scale
case .right:
targetRect.origin.x = rect.minY * scale
targetRect.origin.y = image.size.width - rect.maxX * scale
targetRect.size.width = rect.height
targetRect.size.height = rect.width
targetRect.origin.y = (image.size.width - rect.maxX) * scale
targetRect.size.width = rect.height * scale
targetRect.size.height = rect.width * scale
case .left:
targetRect.origin.x = image.size.height - rect.maxY * scale
targetRect.origin.y = rect.minX * scale
targetRect.size.width = rect.height
targetRect.size.height = rect.width
targetRect.size.width = rect.height * scale
targetRect.size.height = rect.width * scale
default:
targetRect = rect
targetRect = CGRect(x: rect.origin.x * scale,
y: rect.origin.y * scale,
width: rect.width * scale,
height: rect.height * scale)
}

if let croppedCGImage = image.cgImage?.cropping(to: targetRect) {
Expand Down
26 changes: 22 additions & 4 deletions FMPhotoPicker/FMPhotoPicker/source/ImageEditor/FMImageEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,24 @@ public struct FMCropArea {
}

struct FMImageEditor {
var filter: FMFilterable = kDefaultFilter
var crop: FMCroppable = kDefaultCrop
var cropArea: FMCropArea = FMCropArea()
var zoomScale: CGFloat?
var filter: FMFilterable
var crop: FMCroppable
var cropArea: FMCropArea

let initFilter: FMFilterable
let initCrop: FMCroppable
let initCropArea: FMCropArea

init(filter: FMFilterable=kDefaultFilter, crop: FMCroppable=kDefaultCrop, cropArea: FMCropArea=FMCropArea()) {
initFilter = filter
self.filter = filter

initCrop = crop
self.crop = crop

initCropArea = cropArea
self.cropArea = cropArea
}

func reproduce(source image: UIImage, cropState: FMImageEditState, filterState: FMImageEditState) -> UIImage {
var result = image
Expand Down Expand Up @@ -75,4 +89,8 @@ struct FMImageEditor {
return crop.crop(image: image,
toRect: cropArea.area(forSize: image.size))
}

public func isEdited() -> Bool {
return filter.filterName() != initFilter.filterName() || crop.name() != initCrop.name() || !cropArea.isApproximatelyEqual(to: initCropArea)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class FMPhotoPickerImageCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var videoInfoView: UIView!
@IBOutlet weak var videoLengthLabel: UILabel!
@IBOutlet weak var editedMarkImageView: UIImageView!
@IBOutlet weak var editedMarkImageViewTopConstraint: NSLayoutConstraint!

private weak var photoAsset: FMPhotoAsset?

Expand Down Expand Up @@ -48,21 +49,27 @@ class FMPhotoPickerImageCollectionViewCell: UICollectionViewCell {
public func loadView(photoAsset: FMPhotoAsset, selectMode: FMSelectMode, selectedIndex: Int?) {
self.selectMode = selectMode

if selectMode == .single {
self.selectedIndex.isHidden = true
self.selectButton.isHidden = true
self.editedMarkImageViewTopConstraint.constant = 10
}

self.photoAsset = photoAsset

photoAsset.requestThumb() { image in
self.imageView.image = image
}

photoAsset.thumbChanged = { [weak self] image in
guard let strongSelf = self else { return }
photoAsset.thumbChanged = { [weak self, weak photoAsset] image in
guard let strongSelf = self, let strongPhotoAsset = photoAsset else { return }
strongSelf.imageView.image = image
strongSelf.editedMarkImageView.isHidden = !photoAsset.isEdited()
strongSelf.editedMarkImageView.isHidden = !strongPhotoAsset.isEdited()
}

if photoAsset.mediaType == .video {
self.videoInfoView.isHidden = false
self.videoLengthLabel.text = photoAsset.asset.duration.stringTime
self.videoLengthLabel.text = photoAsset.asset?.duration.stringTime
}

self.editedMarkImageView.isHidden = !photoAsset.isEdited()
Expand Down
Loading

0 comments on commit bbe29c4

Please sign in to comment.