Class: Motion::ImageEditorController

Inherits:
UIViewController
  • Object
show all
Defined in:
lib/project/controllers/image_editor_controller.rb

Constant Summary collapse

ANIMATION_DURATION =
0.2
MINIMUM_SCALE =
1
MAXIMUM_SCALE =
3

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#crop_rectObject

Returns the value of attribute crop_rect.



6
7
8
# File 'lib/project/controllers/image_editor_controller.rb', line 6

def crop_rect
  @crop_rect
end

#enforce_bounds=(value) ⇒ Object (writeonly)

Sets the attribute enforce_bounds

Parameters:

  • value

    the value to set the attribute enforce_bounds to.



7
8
9
# File 'lib/project/controllers/image_editor_controller.rb', line 7

def enforce_bounds=(value)
  @enforce_bounds = value
end

#output_widthObject (readonly)

Returns the value of attribute output_width.



6
7
8
# File 'lib/project/controllers/image_editor_controller.rb', line 6

def output_width
  @output_width
end

#rotation_enabled=(value) ⇒ Object (writeonly)

Sets the attribute rotation_enabled

Parameters:

  • value

    the value to set the attribute rotation_enabled to.



7
8
9
# File 'lib/project/controllers/image_editor_controller.rb', line 7

def rotation_enabled=(value)
  @rotation_enabled = value
end

#source_imageObject

Returns the value of attribute source_image.



6
7
8
# File 'lib/project/controllers/image_editor_controller.rb', line 6

def source_image
  @source_image
end

Instance Method Details

#add_gesture_recognizersObject



39
40
41
42
43
44
# File 'lib/project/controllers/image_editor_controller.rb', line 39

def add_gesture_recognizers
  crop_view.addGestureRecognizer(pan_recognizer)
  crop_view.addGestureRecognizer(pinch_recognizer)
  crop_view.addGestureRecognizer(rotation_recognizer)
  crop_view.addGestureRecognizer(tap_recognizer)
end

#add_subviewsObject



34
35
36
37
# File 'lib/project/controllers/image_editor_controller.rb', line 34

def add_subviews
  view.addSubview(crop_view)
  view.insertSubview(image_view, belowSubview: crop_view)
end

#bounded_scale(scale) ⇒ Object



358
359
360
361
362
# File 'lib/project/controllers/image_editor_controller.rb', line 358

def bounded_scale(scale)
  return scale if (MINIMUM_SCALE..MAXIMUM_SCALE).include? scale

  scale < MINIMUM_SCALE ? MINIMUM_SCALE : MAXIMUM_SCALE
end

#check_boundsObject



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/project/controllers/image_editor_controller.rb', line 229

def check_bounds
  y_offset = 0
  x_offset = 0

  if image_view.frame.origin.x > crop_rect.origin.x
    x_offset    = -(image_view.frame.origin.x - crop_rect.origin.x)
    new_right_x = CGRectGetMaxX(image_view.frame) + x_offset

    if new_right_x < CGRectGetMaxX(crop_rect)
      x_offset = CGRectGetMaxX(crop_rect) - CGRectGetMaxX(image_view.frame)
    end
  elsif CGRectGetMaxX(image_view.frame) < CGRectGetMaxX(crop_rect)
    x_offset = CGRectGetMaxX(crop_rect) - CGRectGetMaxX(image_view.frame)
    new_left_x = image_view.frame.origin.x + x_offset

    if new_left_x > crop_rect.origin.x
      x_offset = crop_rect.origin.x - image_view.frame.origin.x
    end
  end

  if image_view.frame.origin.y > crop_rect.origin.y
    y_offset = -(image_view.frame.origin.y - crop_rect.origin.y)
    new_bottom_y = CGRectGetMaxY(image_view.frame) + y_offset

    if new_bottom_y < CGRectGetMaxY(crop_rect)
      y_offset = CGRectGetMaxY(crop_rect) - CGRectGetMaxY(image_view.frame)
    end
  elsif CGRectGetMaxY(image_view.frame) < CGRectGetMaxY(crop_rect)
    y_offset = CGRectGetMaxY(crop_rect) - CGRectGetMaxY(image_view.frame)
    new_top_y = image_view.frame.origin.y + y_offset

    if new_top_y > crop_rect.origin.y
      y_offset = crop_rect.origin.y - image_view.frame.origin.y
    end
  end

  if x_offset || y_offset
    view.userInteractionEnabled = false
    transform = CGAffineTransformTranslate(image_view.transform, x_offset / scale, y_offset / scale)
    UIView.animateWithDuration(
      ANIMATION_DURATION,
      delay:      0,
      options:    UIViewAnimationOptionCurveEaseOut,
      animations: -> { image_view.transform = transform },
      completion: -> (finished) { view.userInteractionEnabled = true })
  end
end

#crop_viewObject



408
409
410
411
412
# File 'lib/project/controllers/image_editor_controller.rb', line 408

def crop_view
  @crop_view ||= Motion::ImageEditorView.alloc.init.tap do |view|
    view.translatesAutoresizingMaskIntoConstraints = false
  end
end

#enforce_bounds?Boolean

Returns:

  • (Boolean)


434
435
436
# File 'lib/project/controllers/image_editor_controller.rb', line 434

def enforce_bounds?
  !!@enforce_bounds
end

#handle_gesture_state?(state) ⇒ Boolean

Returns:

  • (Boolean)


73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/project/controllers/image_editor_controller.rb', line 73

def handle_gesture_state?(state)
  case state
  when UIGestureRecognizerStateEnded || UIGestureRecognizerStateCancelled
    new_scale = bounded_scale(scale)

    delta_x = scale_center.x - image_view.bounds.size.width / 2.0
    delta_y = scale_center.y - image_view.bounds.size.height / 2.0

    transform = CGAffineTransformTranslate(image_view.transform, delta_x, delta_y)

    transform = CGAffineTransformScale(transform, new_scale / scale , new_scale / scale)

    transform = CGAffineTransformTranslate(transform, -delta_x, -delta_y)

    view.userInteractionEnabled = false

    UIView.animateWithDuration(
      ANIMATION_DURATION,
      delay: 0,
      options: UIViewAnimationOptionCurveEaseOut,
      animations: -> { image_view.transform = transform },
      completion: -> (finished) {
        view.userInteractionEnabled = true
        @scale = new_scale
    })

    check_bounds if enforce_bounds?

    false
  else
    true
  end
end

#handle_pan(recognizer) ⇒ Object



307
308
309
310
311
312
313
314
315
316
# File 'lib/project/controllers/image_editor_controller.rb', line 307

def handle_pan(recognizer)
  if handle_gesture_state? recognizer.state
    translation = recognizer.translationInView(image_view)
    transform   = CGAffineTransformTranslate(image_view.transform, translation.x, translation.y)

    image_view.transform = transform

    recognizer.setTranslation(CGPointZero, inView: crop_view)
  end
end

#handle_pinch(recognizer) ⇒ Object



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/project/controllers/image_editor_controller.rb', line 318

def handle_pinch(recognizer)
  if handle_gesture_state? recognizer.state
    if recognizer.state == UIGestureRecognizerStateBegan
      @scale_center = @touch_center
    end

    delta_x = scale_center.x - image_view.bounds.size.width / 2.0
    delta_y = scale_center.y - image_view.bounds.size.height / 2.0

    transform = CGAffineTransformTranslate(image_view.transform, delta_x, delta_y)
    transform = CGAffineTransformScale(transform, recognizer.scale, recognizer.scale)
    transform = CGAffineTransformTranslate(transform, -delta_x, -delta_y)

    @scale *= recognizer.scale

    image_view.transform = transform

    recognizer.scale = 1
  end
end

#handle_rotation(recognizer) ⇒ Object



339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/project/controllers/image_editor_controller.rb', line 339

def handle_rotation(recognizer)
  if rotation_enabled? && handle_gesture_state?(recognizer.state)
    delta_x = touch_center.x - image_view.bounds.size.width / 2
    delta_y = touch_center.y - image_view.bounds.size.height / 2

    transform = CGAffineTransformTranslate(image_view.transform, delta_x, delta_y)
    transform = CGAffineTransformRotate(transform, recognizer.rotation)
    transform = CGAffineTransformTranslate(transform, -delta_x, -delta_y)

    image_view.transform = transform

    recognizer.rotation = 0
  end
end

#handle_tap(recognizer) ⇒ Object



354
355
356
# File 'lib/project/controllers/image_editor_controller.rb', line 354

def handle_tap(recognizer)
  reset(animated: true)
end

#handle_touches(touches) ⇒ Object



293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/project/controllers/image_editor_controller.rb', line 293

def handle_touches(touches)
  @touch_center = CGPointZero

  if touches.count >= 2
    touches.each do |touch|
      touch_location = touch.locationInView(image_view)

      @touch_center = CGPointMake(touch_center.x + touch_location.x, touch_center.y + touch_location.y)
    end

    @touch_center = CGPointMake(touch_center.x / touches.count, touch_center.y / touches.count)
  end
end

#image_viewObject



402
403
404
405
406
# File 'lib/project/controllers/image_editor_controller.rb', line 402

def image_view
  @image_view ||= UIImageView.alloc.init.tap do |view|
    view.translatesAutoresizingMaskIntoConstraints = false
  end
end

#initWithNibName(nib, bundle: bundle) ⇒ Object



9
10
11
12
13
14
# File 'lib/project/controllers/image_editor_controller.rb', line 9

def initWithNibName(nib, bundle: bundle)
  super.tap do |controller|
    controller.rotation_enabled = true
    controller.enforce_bounds   = false
  end
end

#pan_recognizerObject



371
372
373
374
375
376
# File 'lib/project/controllers/image_editor_controller.rb', line 371

def pan_recognizer
  @pan_recognizer ||= UIPanGestureRecognizer.alloc.initWithTarget(self, action: 'handle_pan:').tap do |recognizer|
    recognizer.cancelsTouchesInView = false
    recognizer.delegate             = self
  end
end

#pinch_recognizerObject



391
392
393
394
395
396
# File 'lib/project/controllers/image_editor_controller.rb', line 391

def pinch_recognizer
  @pinch_recognizer ||= UIPinchGestureRecognizer.alloc.initWithTarget(self, action: 'handle_pinch:').tap do |recognizer|
    recognizer.cancelsTouchesInView = false
    recognizer.delegate             = self
  end
end

#prefersStatusBarHiddenObject



426
427
428
# File 'lib/project/controllers/image_editor_controller.rb', line 426

def prefersStatusBarHidden
  true
end

#preview_imageObject



398
399
400
# File 'lib/project/controllers/image_editor_controller.rb', line 398

def preview_image
  @preview_image ||= source_image.resizedImageToFitInSize(view.bounds.size, scaleIfSmaller: false)
end

#process(&block) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/project/controllers/image_editor_controller.rb', line 107

def process(&block)
  view.userInteractionEnabled = false

  Dispatch::Queue.concurrent.async do
    @result_ref = transform_image(
      image_view.transform,
      source_image:       source_image.CGImage,
      source_size:        source_image.size,
      source_orientation: source_image.imageOrientation,
      output_width:       output_width || source_image.size.width,
      crop_rect:          crop_rect,
      image_view_size:    image_view.bounds.size)

    Dispatch::Queue.main.async do
      transformed_image = UIImage.imageWithCGImage(
        @result_ref,
        scale: 1.0,
        orientation: UIImageOrientationUp)

      @result_ref = nil

      view.userInteractionEnabled = true

      block.call(transformed_image)
    end
  end
end

#reset(options = {}) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/project/controllers/image_editor_controller.rb', line 193

def reset(options = {})
  animated = options.fetch(:animated, false)

  w = 0.0
  h = 0.0

  source_aspect = source_image.size.height / source_image.size.width
  crop_aspect   = crop_rect.size.height    / crop_rect.size.width

  if source_aspect > crop_aspect
    w = crop_rect.size.width
    h = source_aspect * w
  else
    h = crop_rect.size.height
    w = h / source_aspect
  end

  @scale = 1

  reset_block = -> {
    image_view.transform = CGAffineTransformIdentity
    image_view.frame     = CGRectMake(CGRectGetMidX(crop_rect) - w / 2, CGRectGetMidY(crop_rect) - h / 2, w, h)
    image_view.transform = CGAffineTransformMakeScale(scale, scale)
  }

  if animated
    view.userInteractionEnabled = false

    UIView.animateWithDuration(ANIMATION_DURATION, animations: reset_block, completion: -> (finished) {
        view.userInteractionEnabled = true
    })
  else
    reset_block.call
  end
end

#rotation_enabled?Boolean

Returns:

  • (Boolean)


430
431
432
# File 'lib/project/controllers/image_editor_controller.rb', line 430

def rotation_enabled?
  !!@rotation_enabled
end

#rotation_recognizerObject



384
385
386
387
388
389
# File 'lib/project/controllers/image_editor_controller.rb', line 384

def rotation_recognizer
  @rotation_recognizer ||= UIRotationGestureRecognizer.alloc.initWithTarget(self, action: 'handle_rotation:').tap do |recognizer|
    recognizer.cancelsTouchesInView = false
    recognizer.delegate             = self
  end
end

#scaleObject



414
415
416
# File 'lib/project/controllers/image_editor_controller.rb', line 414

def scale
  @scale ||= 1
end

#scale_centerObject



422
423
424
# File 'lib/project/controllers/image_editor_controller.rb', line 422

def scale_center
  @scale_center ||= CGPointZero
end

#scale_reset_orientationsObject



364
365
366
367
368
369
# File 'lib/project/controllers/image_editor_controller.rb', line 364

def scale_reset_orientations
  [ UIImageOrientationUpMirrored,
    UIImageOrientationDownMirrored,
    UIImageOrientationLeftMirrored,
    UIImageOrientationRightMirrored ]
end

#setup_constraintsObject



46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/project/controllers/image_editor_controller.rb', line 46

def setup_constraints
  view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(
    'H:|[crop]|',
    options: 0,
    metrics: nil,
    views:   { 'crop' => crop_view }))

  view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(
    'V:|[crop]|',
    options: 0,
    metrics: nil,
    views:   { 'crop' => crop_view }))
end

#tap_recognizerObject



378
379
380
381
382
# File 'lib/project/controllers/image_editor_controller.rb', line 378

def tap_recognizer
  @tap_recognizer ||= UITapGestureRecognizer.alloc.initWithTarget(self, action: 'handle_tap:').tap do |recognizer|
    recognizer.numberOfTapsRequired = 2
  end
end

#touch_centerObject



418
419
420
# File 'lib/project/controllers/image_editor_controller.rb', line 418

def touch_center
  @touch_center ||= CGPointZero
end

#touchesBegan(touches, withEvent: event) ⇒ Object



277
278
279
# File 'lib/project/controllers/image_editor_controller.rb', line 277

def touchesBegan(touches, withEvent: event)
  handle_touches(event.allTouches)
end

#touchesCancelled(touches, withEvent: event) ⇒ Object



289
290
291
# File 'lib/project/controllers/image_editor_controller.rb', line 289

def touchesCancelled(touches, withEvent: event)
  handle_touches(event.allTouches)
end

#touchesEnded(touches, withEvent: event) ⇒ Object



285
286
287
# File 'lib/project/controllers/image_editor_controller.rb', line 285

def touchesEnded(touches, withEvent: event)
  handle_touches(event.allTouches)
end

#touchesMoved(touches, withEvent: event) ⇒ Object



281
282
283
# File 'lib/project/controllers/image_editor_controller.rb', line 281

def touchesMoved(touches, withEvent: event)
  handle_touches(event.allTouches)
end

#transform_image(transform, source_image: source_image, source_size: source_size, source_orientation: source_orientation, output_width: output_width, crop_rect: crop_rect, image_view_size: image_view_size) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/project/controllers/image_editor_controller.rb', line 135

def transform_image(transform, source_image: source_image, source_size: source_size, source_orientation: source_orientation, output_width: output_width, crop_rect: crop_rect, image_view_size: image_view_size)
  aspect = crop_rect.size.height / crop_rect.size.width
  output_size = CGSizeMake(output_width, output_width * aspect)

  transpose = false
  orientation_transform = CGAffineTransformIdentity

  case source_orientation
  when UIImageOrientationDown || UIImageOrientationDownMirrored
    orientation_transform = CGAffineTransformMakeRotation(Math::PI)
  when UIImageOrientationLeft || UIImageOrientationLeftMirrored
    orientation_transform = CGAffineTransformMakeRotation(Math::PI / 2.0)
    transpose             = true
  when UIImageOrientationRight || UIImageOrientationRightMirrored
    orientation_transform = CGAffineTransformMakeRotation(-(Math::PI / 2.0))
    transpose             = true
  end

  if scale_reset_orientations.include? source_orientation
    orientation_transform = CGAffineTransformScale(transform, -1, 1)
  end

  if transpose
    image_view_size = CGSizeMake(image_view_size.height, image_view_size.width)
  end

  context = CGBitmapContextCreate(
    nil,                                      # data
    output_size.width,                        # width
    output_size.height,                       # height
    CGImageGetBitsPerComponent(source_image), # bits per component
    0,                                        # bytes per row
    CGImageGetColorSpace(source_image),       # color space
    CGImageGetBitmapInfo(source_image))       # bitmap info

  CGContextSetFillColorWithColor(context,  UIColor.clearColor.CGColor)
  CGContextFillRect(context, CGRectMake(0, 0, output_size.width, output_size.height))

  ui_coords = CGAffineTransformMakeScale(output_size.width / crop_rect.size.width,
                                         output_size.height / crop_rect.size.height)

  ui_coords = CGAffineTransformTranslate(ui_coords, crop_rect.size.width / 2.0,
                                         crop_rect.size.height / 2.0)

  ui_coords = CGAffineTransformScale(ui_coords, 1.0, -1.0)
  CGContextConcatCTM(context, ui_coords)

  CGContextConcatCTM(context, transform)
  CGContextScaleCTM(context, 1.0, -1.0)
  CGContextConcatCTM(context, orientation_transform)

  drawing_rect = CGRectMake(-image_view_size.width / 2.0, -image_view_size.height / 2.0, image_view_size.width, image_view_size.height)

  CGContextDrawImage(context, drawing_rect, source_image)

  CGBitmapContextCreateImage(context)
end

#viewDidAppear(animated) ⇒ Object



26
27
28
29
30
31
32
# File 'lib/project/controllers/image_editor_controller.rb', line 26

def viewDidAppear(animated)
  super

  image_view.image = preview_image

  reset
end

#viewDidLoadObject



16
17
18
19
20
21
22
23
24
# File 'lib/project/controllers/image_editor_controller.rb', line 16

def viewDidLoad
  super

  add_subviews

  add_gesture_recognizers

  setup_constraints
end