into(@NonNull OutputStream outputStream, boolean closeWhenDone) {
63 | final Bitmap croppedBitmap = cropView.crop();
64 | return Utils.flushToStream(croppedBitmap, format, quality, outputStream, closeWhenDone);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/CropView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Lyft, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.lyft.android.scissors2;
17 |
18 | import android.app.Activity;
19 | import android.app.Fragment;
20 | import android.content.Context;
21 | import android.content.Intent;
22 | import android.graphics.Bitmap;
23 | import android.graphics.BitmapFactory;
24 | import android.graphics.Canvas;
25 | import android.graphics.Matrix;
26 | import android.graphics.Paint;
27 | import android.graphics.Path;
28 | import android.graphics.RectF;
29 | import android.graphics.drawable.BitmapDrawable;
30 | import android.graphics.drawable.Drawable;
31 | import android.net.Uri;
32 | import android.support.annotation.ColorInt;
33 | import android.support.annotation.DrawableRes;
34 | import android.support.annotation.IntDef;
35 | import android.support.annotation.NonNull;
36 | import android.support.annotation.Nullable;
37 | import android.util.AttributeSet;
38 | import android.view.MotionEvent;
39 | import android.widget.ImageView;
40 | import java.io.File;
41 | import java.io.OutputStream;
42 | import java.lang.annotation.Retention;
43 | import java.lang.annotation.RetentionPolicy;
44 |
45 | /**
46 | * An {@link ImageView} with a fixed viewport and cropping capabilities.
47 | */
48 | public class CropView extends ImageView {
49 |
50 | private TouchManager touchManager;
51 | private CropViewConfig config;
52 |
53 | private Paint viewportPaint = new Paint();
54 | private Paint bitmapPaint = new Paint();
55 |
56 | private Bitmap bitmap;
57 | private Matrix transform = new Matrix();
58 | private Extensions extensions;
59 |
60 | /** Corresponds to the values in {@link com.lyft.android.scissors2.R.attr#cropviewShape} */
61 | @Retention(RetentionPolicy.SOURCE)
62 | @IntDef({ Shape.RECTANGLE, Shape.OVAL })
63 | public @interface Shape {
64 |
65 | int RECTANGLE = 0;
66 | int OVAL = 1;
67 | }
68 |
69 | @Shape
70 | private int shape = Shape.RECTANGLE;
71 | private Path ovalPath;
72 | private RectF ovalRect;
73 |
74 | public CropView(Context context) {
75 | super(context);
76 | initCropView(context, null);
77 | }
78 |
79 | public CropView(Context context, AttributeSet attrs) {
80 | super(context, attrs);
81 |
82 | initCropView(context, attrs);
83 | }
84 |
85 | void initCropView(Context context, AttributeSet attrs) {
86 | config = CropViewConfig.from(context, attrs);
87 |
88 | touchManager = new TouchManager(this, config);
89 |
90 | bitmapPaint.setFilterBitmap(true);
91 | setViewportOverlayColor(config.getViewportOverlayColor());
92 | shape = config.shape();
93 |
94 | // We need anti-aliased Paint to smooth the curved edges
95 | viewportPaint.setFlags(viewportPaint.getFlags() | Paint.ANTI_ALIAS_FLAG);
96 | }
97 |
98 | @Override
99 | protected void onDraw(Canvas canvas) {
100 | super.onDraw(canvas);
101 |
102 | if (bitmap == null) {
103 | return;
104 | }
105 |
106 | drawBitmap(canvas);
107 | if (shape == Shape.RECTANGLE) {
108 | drawSquareOverlay(canvas);
109 | } else {
110 | drawOvalOverlay(canvas);
111 | }
112 | }
113 |
114 | private void drawBitmap(Canvas canvas) {
115 | transform.reset();
116 | touchManager.applyPositioningAndScale(transform);
117 |
118 | canvas.drawBitmap(bitmap, transform, bitmapPaint);
119 | }
120 |
121 | private void drawSquareOverlay(Canvas canvas) {
122 | final int viewportWidth = touchManager.getViewportWidth();
123 | final int viewportHeight = touchManager.getViewportHeight();
124 | final int left = (getWidth() - viewportWidth) / 2;
125 | final int top = (getHeight() - viewportHeight) / 2;
126 |
127 | canvas.drawRect(0, top, left, getHeight() - top, viewportPaint); // left
128 | canvas.drawRect(0, 0, getWidth(), top, viewportPaint); // top
129 | canvas.drawRect(getWidth() - left, top, getWidth(), getHeight() - top, viewportPaint); // right
130 | canvas.drawRect(0, getHeight() - top, getWidth(), getHeight(), viewportPaint); // bottom
131 | }
132 |
133 | private void drawOvalOverlay(Canvas canvas) {
134 | if (ovalRect == null) {
135 | ovalRect = new RectF();
136 | }
137 | if (ovalPath == null) {
138 | ovalPath = new Path();
139 | }
140 |
141 | final int viewportWidth = touchManager.getViewportWidth();
142 | final int viewportHeight = touchManager.getViewportHeight();
143 | final int left = (getWidth() - viewportWidth) / 2;
144 | final int top = (getHeight() - viewportHeight) / 2;
145 | final int right = getWidth() - left;
146 | final int bottom = getHeight() - top;
147 | ovalRect.left = left;
148 | ovalRect.top = top;
149 | ovalRect.right = right;
150 | ovalRect.bottom = bottom;
151 |
152 | // top left arc
153 | ovalPath.reset();
154 | ovalPath.moveTo(left, getHeight() / 2); // middle of the left side of the circle
155 | ovalPath.arcTo(ovalRect, 180, 90, false); // draw arc to top
156 | ovalPath.lineTo(left, top); // move to top-left corner
157 | ovalPath.lineTo(left, getHeight() / 2); // move back to origin
158 | ovalPath.close();
159 | canvas.drawPath(ovalPath, viewportPaint);
160 |
161 | // top right arc
162 | ovalPath.reset();
163 | ovalPath.moveTo(getWidth() / 2, top); // middle of the top side of the circle
164 | ovalPath.arcTo(ovalRect, 270, 90, false); // draw arc to the right
165 | ovalPath.lineTo(right, top); // move to top-right corner
166 | ovalPath.lineTo(getWidth() / 2, top); // move back to origin
167 | ovalPath.close();
168 | canvas.drawPath(ovalPath, viewportPaint);
169 |
170 | // bottom right arc
171 | ovalPath.reset();
172 | ovalPath.moveTo(right, getHeight() / 2); // middle of the right side of the circle
173 | ovalPath.arcTo(ovalRect, 0, 90, false); // draw arc to the bottom
174 | ovalPath.lineTo(right, bottom); // move to bottom-right corner
175 | ovalPath.lineTo(right, getHeight() / 2); // move back to origin
176 | ovalPath.close();
177 | canvas.drawPath(ovalPath, viewportPaint);
178 |
179 | // bottom left arc
180 | ovalPath.reset();
181 | ovalPath.moveTo(getWidth() / 2, bottom); // middle of the bottom side of the circle
182 | ovalPath.arcTo(ovalRect, 90, 90, false); // draw arc to the left
183 | ovalPath.lineTo(left, bottom); // move to bottom-left corner
184 | ovalPath.lineTo(getWidth() / 2, bottom); // move back to origin
185 | ovalPath.close();
186 | canvas.drawPath(ovalPath, viewportPaint);
187 |
188 | // Draw the square overlay as well
189 | drawSquareOverlay(canvas);
190 | }
191 |
192 | @Override
193 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
194 | super.onSizeChanged(w, h, oldw, oldh);
195 | resetTouchManager();
196 | }
197 |
198 | /**
199 | * Sets the color of the viewport overlay
200 | *
201 | * @param viewportOverlayColor The color to use for the viewport overlay
202 | */
203 | public void setViewportOverlayColor(@ColorInt int viewportOverlayColor) {
204 | viewportPaint.setColor(viewportOverlayColor);
205 | config.setViewportOverlayColor(viewportOverlayColor);
206 | }
207 |
208 | /**
209 | * Sets the padding for the viewport overlay
210 | *
211 | * @param viewportOverlayPadding The new padding of the viewport overlay
212 | */
213 | public void setViewportOverlayPadding(int viewportOverlayPadding) {
214 | config.setViewportOverlayPadding(viewportOverlayPadding);
215 | resetTouchManager();
216 | invalidate();
217 | }
218 |
219 | /**
220 | * Returns the native aspect ratio of the image.
221 | *
222 | * @return The native aspect ratio of the image.
223 | */
224 | public float getImageRatio() {
225 | Bitmap bitmap = getImageBitmap();
226 | return bitmap != null ? (float) bitmap.getWidth() / (float) bitmap.getHeight() : 0f;
227 | }
228 |
229 | /**
230 | * Returns the aspect ratio of the viewport and crop rect.
231 | *
232 | * @return The current viewport aspect ratio.
233 | */
234 | public float getViewportRatio() {
235 | return touchManager.getAspectRatio();
236 | }
237 |
238 | /**
239 | * Sets the aspect ratio of the viewport and crop rect. Defaults to
240 | * the native aspect ratio if ratio == 0
.
241 | *
242 | * @param ratio The new aspect ratio of the viewport.
243 | */
244 | public void setViewportRatio(float ratio) {
245 | if (Float.compare(ratio, 0) == 0) {
246 | ratio = getImageRatio();
247 | }
248 | touchManager.setAspectRatio(ratio);
249 | resetTouchManager();
250 | invalidate();
251 | }
252 |
253 | @Override
254 | public void setImageResource(@DrawableRes int resId) {
255 | final Bitmap bitmap = resId > 0
256 | ? BitmapFactory.decodeResource(getResources(), resId)
257 | : null;
258 | setImageBitmap(bitmap);
259 | }
260 |
261 | @Override
262 | public void setImageDrawable(@Nullable Drawable drawable) {
263 | final Bitmap bitmap;
264 | if (drawable instanceof BitmapDrawable) {
265 | BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
266 | bitmap = bitmapDrawable.getBitmap();
267 | } else if (drawable != null) {
268 | bitmap = Utils.asBitmap(drawable, getWidth(), getHeight());
269 | } else {
270 | bitmap = null;
271 | }
272 |
273 | setImageBitmap(bitmap);
274 | }
275 |
276 | @Override
277 | public void setImageURI(@Nullable Uri uri) {
278 | extensions().load(uri);
279 | }
280 |
281 | @Override
282 | public void setImageBitmap(@Nullable Bitmap bitmap) {
283 | this.bitmap = bitmap;
284 | resetTouchManager();
285 | invalidate();
286 | }
287 |
288 | /**
289 | * @return Current working Bitmap or null
if none has been set yet.
290 | */
291 | @Nullable
292 | public Bitmap getImageBitmap() {
293 | return bitmap;
294 | }
295 |
296 | private void resetTouchManager() {
297 | final boolean invalidBitmap = bitmap == null;
298 | final int bitmapWidth = invalidBitmap ? 0 : bitmap.getWidth();
299 | final int bitmapHeight = invalidBitmap ? 0 : bitmap.getHeight();
300 | touchManager.resetFor(bitmapWidth, bitmapHeight, getWidth(), getHeight());
301 | }
302 |
303 | @Override
304 | public boolean dispatchTouchEvent(MotionEvent event) {
305 | boolean result = super.dispatchTouchEvent(event);
306 |
307 | if (!isEnabled()) {
308 | return result;
309 | }
310 |
311 | touchManager.onEvent(event);
312 | invalidate();
313 | return true;
314 | }
315 |
316 | /**
317 | * Performs synchronous image cropping based on configuration.
318 | *
319 | * @return A {@link Bitmap} cropped based on viewport and user panning and zooming or null
if no {@link Bitmap} has been
320 | * provided.
321 | */
322 | @Nullable
323 | public Bitmap crop() {
324 | if (bitmap == null) {
325 | return null;
326 | }
327 |
328 | final Bitmap src = bitmap;
329 | final Bitmap.Config srcConfig = src.getConfig();
330 | final Bitmap.Config config = srcConfig == null ? Bitmap.Config.ARGB_8888 : srcConfig;
331 | final int viewportHeight = touchManager.getViewportHeight();
332 | final int viewportWidth = touchManager.getViewportWidth();
333 |
334 | final Bitmap dst = Bitmap.createBitmap(viewportWidth, viewportHeight, config);
335 |
336 | Canvas canvas = new Canvas(dst);
337 | final int left = (getRight() - viewportWidth) / 2;
338 | final int top = (getBottom() - viewportHeight) / 2;
339 | canvas.translate(-left, -top);
340 |
341 | drawBitmap(canvas);
342 |
343 | return dst;
344 | }
345 |
346 | /**
347 | * Obtain current viewport width.
348 | *
349 | * @return Current viewport width.
350 | * Note: It might be 0 if layout pass has not been completed.
351 | */
352 | public int getViewportWidth() {
353 | return touchManager.getViewportWidth();
354 | }
355 |
356 | /**
357 | * Obtain current viewport height.
358 | *
359 | * @return Current viewport height.
360 | * Note: It might be 0 if layout pass has not been completed.
361 | */
362 | public int getViewportHeight() {
363 | return touchManager.getViewportHeight();
364 | }
365 |
366 | /**
367 | * Offers common utility extensions.
368 | *
369 | * @return Extensions object used to perform chained calls.
370 | */
371 | public Extensions extensions() {
372 | if (extensions == null) {
373 | extensions = new Extensions(this);
374 | }
375 | return extensions;
376 | }
377 |
378 | /**
379 | * Get the transform matrix
380 | */
381 | public Matrix getTransformMatrix() {
382 | return transform;
383 | }
384 |
385 | /**
386 | * Optional extensions to perform common actions involving a {@link CropView}
387 | */
388 | public static class Extensions {
389 |
390 | private final CropView cropView;
391 |
392 | Extensions(CropView cropView) {
393 | this.cropView = cropView;
394 | }
395 |
396 | /**
397 | * Load a {@link Bitmap} using an automatically resolved {@link BitmapLoader} which will attempt to scale image to fill view.
398 | *
399 | * @param model Model used by {@link BitmapLoader} to load desired {@link Bitmap}
400 | * @see PicassoBitmapLoader
401 | * @see GlideBitmapLoader
402 | */
403 | public void load(@Nullable Object model) {
404 | new LoadRequest(cropView)
405 | .load(model);
406 | }
407 |
408 | /**
409 | * Load a {@link Bitmap} using given {@link BitmapLoader}, you must call {@link LoadRequest#load(Object)} afterwards.
410 | *
411 | * @param bitmapLoader {@link BitmapLoader} used to load desired {@link Bitmap}
412 | * @see PicassoBitmapLoader
413 | * @see GlideBitmapLoader
414 | */
415 | public LoadRequest using(@Nullable BitmapLoader bitmapLoader) {
416 | return new LoadRequest(cropView).using(bitmapLoader);
417 | }
418 |
419 | public enum LoaderType {
420 | PICASSO,
421 | GLIDE,
422 | UIL,
423 | CLASS_LOOKUP
424 | }
425 |
426 | /**
427 | * Load a {@link Bitmap} using a reference to a {@link BitmapLoader}, you must call {@link LoadRequest#load(Object)} afterwards.
428 | *
429 | * Please ensure that the library for the {@link BitmapLoader} you reference is available on the classpath.
430 | *
431 | * @param loaderType the {@link BitmapLoader} to use to load desired (@link Bitmap}
432 | * @see PicassoBitmapLoader
433 | * @see GlideBitmapLoader
434 | */
435 | public LoadRequest using(@NonNull LoaderType loaderType) {
436 | return new LoadRequest(cropView).using(loaderType);
437 | }
438 |
439 | /**
440 | * Perform an asynchronous crop request.
441 | *
442 | * @return {@link CropRequest} used to chain a configure cropping request, you must call either one of:
443 | *
444 | * - {@link CropRequest#into(File)}
445 | * - {@link CropRequest#into(OutputStream, boolean)}
446 | *
447 | */
448 | public CropRequest crop() {
449 | return new CropRequest(cropView);
450 | }
451 |
452 | /**
453 | * Perform a pick image request using {@link Activity#startActivityForResult(Intent, int)}.
454 | */
455 | public void pickUsing(@NonNull Activity activity, int requestCode) {
456 | CropViewExtensions.pickUsing(activity, requestCode);
457 | }
458 |
459 | /**
460 | * Perform a pick image request using {@link Fragment#startActivityForResult(Intent, int)}.
461 | */
462 | public void pickUsing(@NonNull Fragment fragment, int requestCode) {
463 | CropViewExtensions.pickUsing(fragment, requestCode);
464 | }
465 | }
466 | }
467 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/CropViewConfig.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Lyft, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.lyft.android.scissors2;
17 |
18 | import android.content.Context;
19 | import android.content.res.TypedArray;
20 | import android.util.AttributeSet;
21 |
22 | class CropViewConfig {
23 |
24 | public static final float DEFAULT_VIEWPORT_RATIO = 0f;
25 | public static final float DEFAULT_MAXIMUM_SCALE = 10f;
26 | public static final float DEFAULT_MINIMUM_SCALE = 0f;
27 | public static final int DEFAULT_IMAGE_QUALITY = 100;
28 | public static final int DEFAULT_VIEWPORT_OVERLAY_PADDING = 0;
29 | public static final int DEFAULT_VIEWPORT_OVERLAY_COLOR = 0xC8000000; // Black with 200 alpha
30 | public static final int DEFAULT_SHAPE = CropView.Shape.RECTANGLE;
31 |
32 | private float viewportRatio = DEFAULT_VIEWPORT_RATIO;
33 | private float maxScale = DEFAULT_MAXIMUM_SCALE;
34 | private float minScale = DEFAULT_MINIMUM_SCALE;
35 | private int viewportOverlayPadding = DEFAULT_VIEWPORT_OVERLAY_PADDING;
36 | private int viewportOverlayColor = DEFAULT_VIEWPORT_OVERLAY_COLOR;
37 | private @CropView.Shape int shape = DEFAULT_SHAPE;
38 |
39 | public int getViewportOverlayColor() {
40 | return viewportOverlayColor;
41 | }
42 |
43 | void setViewportOverlayColor(int viewportOverlayColor) {
44 | this.viewportOverlayColor = viewportOverlayColor;
45 | }
46 |
47 | public int getViewportOverlayPadding() {
48 | return viewportOverlayPadding;
49 | }
50 |
51 | void setViewportOverlayPadding(int viewportOverlayPadding) {
52 | this.viewportOverlayPadding = viewportOverlayPadding;
53 | }
54 |
55 | public float getViewportRatio() {
56 | return viewportRatio;
57 | }
58 |
59 | void setViewportRatio(float viewportRatio) {
60 | this.viewportRatio = viewportRatio <= 0 ? DEFAULT_VIEWPORT_RATIO : viewportRatio;
61 | }
62 |
63 | public float getMaxScale() {
64 | return maxScale;
65 | }
66 |
67 | void setMaxScale(float maxScale) {
68 | this.maxScale = maxScale <= 0 ? DEFAULT_MAXIMUM_SCALE : maxScale;
69 | }
70 |
71 | public float getMinScale() {
72 | return minScale;
73 | }
74 |
75 | void setMinScale(float minScale) {
76 | this.minScale = minScale <= 0 ? DEFAULT_MINIMUM_SCALE : minScale;
77 | }
78 |
79 | public @CropView.Shape int shape() {
80 | return shape;
81 | }
82 |
83 | public void setShape(@CropView.Shape int shape) {
84 | this.shape = shape;
85 | }
86 |
87 | public static CropViewConfig from(Context context, AttributeSet attrs) {
88 | final CropViewConfig cropViewConfig = new CropViewConfig();
89 |
90 | if (attrs == null) {
91 | return cropViewConfig;
92 | }
93 |
94 | TypedArray attributes = context.obtainStyledAttributes(
95 | attrs,
96 | R.styleable.CropView);
97 |
98 | cropViewConfig.setViewportRatio(
99 | attributes.getFloat(R.styleable.CropView_cropviewViewportRatio,
100 | CropViewConfig.DEFAULT_VIEWPORT_RATIO));
101 |
102 | cropViewConfig.setMaxScale(
103 | attributes.getFloat(R.styleable.CropView_cropviewMaxScale,
104 | CropViewConfig.DEFAULT_MAXIMUM_SCALE));
105 |
106 | cropViewConfig.setMinScale(
107 | attributes.getFloat(R.styleable.CropView_cropviewMinScale,
108 | CropViewConfig.DEFAULT_MINIMUM_SCALE));
109 |
110 | cropViewConfig.setViewportOverlayColor(
111 | attributes.getColor(R.styleable.CropView_cropviewViewportOverlayColor,
112 | CropViewConfig.DEFAULT_VIEWPORT_OVERLAY_COLOR));
113 |
114 | cropViewConfig.setViewportOverlayPadding(
115 | attributes.getDimensionPixelSize(R.styleable.CropView_cropviewViewportOverlayPadding,
116 | CropViewConfig.DEFAULT_VIEWPORT_OVERLAY_PADDING));
117 |
118 | @CropView.Shape int shape = attributes.getInt(
119 | R.styleable.CropView_cropviewShape, CropViewConfig.DEFAULT_SHAPE);
120 | cropViewConfig.setShape(shape);
121 |
122 | attributes.recycle();
123 |
124 | return cropViewConfig;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/CropViewExtensions.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Lyft, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.lyft.android.scissors2;
17 |
18 | import android.app.Activity;
19 | import android.app.Fragment;
20 | import android.content.Intent;
21 | import android.graphics.Rect;
22 |
23 | import static com.lyft.android.scissors2.CropView.Extensions.LoaderType;
24 |
25 | class CropViewExtensions {
26 |
27 | static void pickUsing(Activity activity, int requestCode) {
28 | activity.startActivityForResult(
29 | createChooserIntent(),
30 | requestCode);
31 | }
32 |
33 | static void pickUsing(Fragment fragment, int requestCode) {
34 | fragment.startActivityForResult(
35 | createChooserIntent(),
36 | requestCode);
37 | }
38 |
39 | private static Intent createChooserIntent() {
40 | Intent intent = new Intent();
41 | intent.setType("image/*");
42 | intent.setAction(Intent.ACTION_GET_CONTENT);
43 | intent.addCategory(Intent.CATEGORY_OPENABLE);
44 |
45 | return Intent.createChooser(intent, null);
46 | }
47 |
48 | final static boolean HAS_PICASSO = canHasClass("com.squareup.picasso.Picasso");
49 | final static boolean HAS_GLIDE = canHasClass("com.bumptech.glide.Glide");
50 | final static boolean HAS_UIL = canHasClass("com.nostra13.universalimageloader.core.ImageLoader");
51 |
52 | static BitmapLoader resolveBitmapLoader(CropView cropView, LoaderType loaderType) {
53 | switch (loaderType) {
54 | case PICASSO:
55 | return PicassoBitmapLoader.createUsing(cropView);
56 | case GLIDE:
57 | return GlideBitmapLoader.createUsing(cropView);
58 | case UIL:
59 | return UILBitmapLoader.createUsing(cropView);
60 | case CLASS_LOOKUP:
61 | break;
62 | default:
63 | throw new IllegalStateException("Unsupported type of loader = " + loaderType);
64 | }
65 |
66 | if (HAS_PICASSO) {
67 | return PicassoBitmapLoader.createUsing(cropView);
68 | }
69 | if (HAS_GLIDE) {
70 | return GlideBitmapLoader.createUsing(cropView);
71 | }
72 | if (HAS_UIL) {
73 | return UILBitmapLoader.createUsing(cropView);
74 | }
75 | throw new IllegalStateException("You must provide a BitmapLoader.");
76 | }
77 |
78 | static boolean canHasClass(String className) {
79 | try {
80 | Class.forName(className);
81 | return true;
82 | } catch (ClassNotFoundException e) {
83 | }
84 | return false;
85 | }
86 |
87 | static Rect computeTargetSize(int sourceWidth, int sourceHeight, int viewportWidth, int viewportHeight) {
88 |
89 | if (sourceWidth == viewportWidth && sourceHeight == viewportHeight) {
90 | return new Rect(0, 0, viewportWidth, viewportHeight); // Fail fast for when source matches exactly on viewport
91 | }
92 |
93 | float scale;
94 | if (sourceWidth * viewportHeight > viewportWidth * sourceHeight) {
95 | scale = (float) viewportHeight / (float) sourceHeight;
96 | } else {
97 | scale = (float) viewportWidth / (float) sourceWidth;
98 | }
99 | final int recommendedWidth = (int) ((sourceWidth * scale) + 0.5f);
100 | final int recommendedHeight = (int) ((sourceHeight * scale) + 0.5f);
101 | return new Rect(0, 0, recommendedWidth, recommendedHeight);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/GlideBitmapLoader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Lyft, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.lyft.android.scissors2;
17 |
18 | import android.support.annotation.NonNull;
19 | import android.support.annotation.Nullable;
20 | import android.widget.ImageView;
21 |
22 | import com.bumptech.glide.Glide;
23 | import com.bumptech.glide.RequestManager;
24 | import com.bumptech.glide.load.engine.DiskCacheStrategy;
25 | import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
26 | import com.bumptech.glide.request.RequestOptions;
27 |
28 | /**
29 | * A {@link BitmapLoader} with transformation for {@link Glide} image library.
30 | *
31 | * @see GlideBitmapLoader#createUsing(CropView)
32 | * @see GlideBitmapLoader#createUsing(CropView, RequestManager)
33 | */
34 | public class GlideBitmapLoader implements BitmapLoader {
35 |
36 | private final RequestManager requestManager;
37 | private final BitmapTransformation transformation;
38 |
39 | public GlideBitmapLoader(@NonNull RequestManager requestManager, @NonNull BitmapTransformation transformation) {
40 | this.requestManager = requestManager;
41 | this.transformation = transformation;
42 | }
43 |
44 | @Override
45 | public void load(@Nullable Object model, @NonNull ImageView imageView) {
46 | RequestOptions requestOptions = new RequestOptions();
47 | requestOptions.skipMemoryCache(true)
48 | .diskCacheStrategy(DiskCacheStrategy.DATA)
49 | .transform(transformation);
50 |
51 | requestManager.asBitmap()
52 | .load(model)
53 | .apply(requestOptions);
54 | }
55 |
56 | public static BitmapLoader createUsing(@NonNull CropView cropView) {
57 | return createUsing(cropView, Glide.with(cropView.getContext()));
58 | }
59 |
60 | public static BitmapLoader createUsing(@NonNull CropView cropView, @NonNull RequestManager requestManager) {
61 | return new GlideBitmapLoader(requestManager,
62 | GlideFillViewportTransformation.createUsing(cropView.getViewportWidth(), cropView.getViewportHeight()));
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/GlideFillViewportTransformation.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Lyft, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.lyft.android.scissors2;
17 |
18 | import android.graphics.Bitmap;
19 | import android.graphics.Rect;
20 |
21 | import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
22 | import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
23 |
24 | import java.nio.charset.Charset;
25 | import java.security.MessageDigest;
26 |
27 | class GlideFillViewportTransformation extends BitmapTransformation {
28 |
29 | private static final String ID = "com.lyft.android.scissors.GlideFillViewportTransformation";
30 | private static final byte[] ID_BYTES = ID.getBytes(Charset.defaultCharset());
31 |
32 | private final int viewportWidth;
33 | private final int viewportHeight;
34 |
35 | public GlideFillViewportTransformation(int viewportWidth, int viewportHeight) {
36 | this.viewportWidth = viewportWidth;
37 | this.viewportHeight = viewportHeight;
38 | }
39 |
40 | @Override
41 | protected Bitmap transform(BitmapPool bitmapPool, Bitmap source, int outWidth, int outHeight) {
42 | int sourceWidth = source.getWidth();
43 | int sourceHeight = source.getHeight();
44 |
45 | Rect target = CropViewExtensions.computeTargetSize(sourceWidth, sourceHeight, viewportWidth, viewportHeight);
46 |
47 | int targetWidth = target.width();
48 | int targetHeight = target.height();
49 |
50 | return Bitmap.createScaledBitmap(
51 | source,
52 | targetWidth,
53 | targetHeight,
54 | true);
55 | }
56 |
57 | @Override
58 | public boolean equals(Object obj) {
59 | if (obj instanceof GlideFillViewportTransformation) {
60 | GlideFillViewportTransformation other = (GlideFillViewportTransformation) obj;
61 | return other.viewportWidth == viewportWidth && other.viewportHeight == viewportHeight;
62 | }
63 | return false;
64 | }
65 |
66 | @Override
67 | public int hashCode() {
68 | int hash = viewportWidth * 31 + viewportHeight;
69 | return hash * 17 + ID.hashCode();
70 | }
71 |
72 | @Override
73 | public void updateDiskCacheKey(MessageDigest messageDigest) {
74 | messageDigest.update(ID_BYTES);
75 | }
76 |
77 | public static BitmapTransformation createUsing(int viewportWidth, int viewportHeight) {
78 | return new GlideFillViewportTransformation(viewportWidth, viewportHeight);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/LoadRequest.java:
--------------------------------------------------------------------------------
1 | package com.lyft.android.scissors2;
2 |
3 | import android.graphics.Bitmap;
4 | import android.support.annotation.Nullable;
5 | import android.view.ViewTreeObserver;
6 |
7 | import static com.lyft.android.scissors2.CropView.Extensions.LoaderType;
8 | import static com.lyft.android.scissors2.CropViewExtensions.resolveBitmapLoader;
9 |
10 | public class LoadRequest {
11 |
12 | private final CropView cropView;
13 | private BitmapLoader bitmapLoader;
14 | private LoaderType loaderType = LoaderType.CLASS_LOOKUP;
15 |
16 | LoadRequest(CropView cropView) {
17 | Utils.checkNotNull(cropView, "cropView == null");
18 | this.cropView = cropView;
19 | }
20 |
21 | /**
22 | * Load a {@link Bitmap} using given {@link BitmapLoader}, you must call {@link LoadRequest#load(Object)} afterwards.
23 | *
24 | * @param bitmapLoader {@link BitmapLoader} to use
25 | * @return current request for chaining, you should call {@link #load(Object)} afterwards.
26 | */
27 | public LoadRequest using(@Nullable BitmapLoader bitmapLoader) {
28 | this.bitmapLoader = bitmapLoader;
29 | return this;
30 | }
31 |
32 | /**
33 | * Load a {@link Bitmap} using the {@link BitmapLoader} specified by {@code loaderType}, you must call {@link
34 | * LoadRequest#load(Object)} afterwards.
35 | *
36 | * @param loaderType a reference to the {@link BitmapLoader} to use
37 | * @return current request for chaining, you should call {@link #load(Object)} afterwards.
38 | */
39 | public LoadRequest using(LoaderType loaderType) {
40 | this.loaderType = loaderType;
41 | return this;
42 | }
43 |
44 | /**
45 | * Load a {@link Bitmap} using a {@link BitmapLoader} into {@link CropView}
46 | *
47 | * @param model Model used by {@link BitmapLoader} to load desired {@link Bitmap}
48 | */
49 | public void load(@Nullable Object model) {
50 | if (cropView.getWidth() == 0 && cropView.getHeight() == 0) {
51 | // Defer load until layout pass
52 | deferLoad(model);
53 | return;
54 | }
55 | performLoad(model);
56 | }
57 |
58 | void performLoad(Object model) {
59 | if (bitmapLoader == null) {
60 | bitmapLoader = resolveBitmapLoader(cropView, loaderType);
61 | }
62 | bitmapLoader.load(model, cropView);
63 | }
64 |
65 | void deferLoad(final Object model) {
66 | if (!cropView.getViewTreeObserver().isAlive()) {
67 | return;
68 | }
69 | cropView.getViewTreeObserver().addOnGlobalLayoutListener(
70 | new ViewTreeObserver.OnGlobalLayoutListener() {
71 | @Override
72 | public void onGlobalLayout() {
73 | if (cropView.getViewTreeObserver().isAlive()) {
74 | //noinspection deprecation
75 | cropView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
76 | }
77 | performLoad(model);
78 | }
79 | }
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/PicassoBitmapLoader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Lyft, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.lyft.android.scissors2;
17 |
18 | import android.net.Uri;
19 | import android.support.annotation.NonNull;
20 | import android.support.annotation.Nullable;
21 | import android.widget.ImageView;
22 | import com.squareup.picasso.Picasso;
23 | import com.squareup.picasso.RequestCreator;
24 | import com.squareup.picasso.Transformation;
25 | import java.io.File;
26 |
27 | /**
28 | * A {@link BitmapLoader} with transformation for {@link Picasso} image library.
29 | *
30 | * @see PicassoBitmapLoader#createUsing(CropView)
31 | * @see PicassoBitmapLoader#createUsing(CropView, Picasso)
32 | */
33 | public class PicassoBitmapLoader implements BitmapLoader {
34 |
35 | private final Picasso picasso;
36 | private final Transformation transformation;
37 |
38 | public PicassoBitmapLoader(Picasso picasso, Transformation transformation) {
39 | this.picasso = picasso;
40 | this.transformation = transformation;
41 | }
42 |
43 | @Override
44 | public void load(@Nullable Object model, @NonNull ImageView imageView) {
45 | final RequestCreator requestCreator;
46 |
47 | if (model instanceof Uri || model == null) {
48 | requestCreator = picasso.load((Uri) model);
49 | } else if (model instanceof String) {
50 | requestCreator = picasso.load((String) model);
51 | } else if (model instanceof File) {
52 | requestCreator = picasso.load((File) model);
53 | } else if (model instanceof Integer) {
54 | requestCreator = picasso.load((Integer) model);
55 | } else {
56 | throw new IllegalArgumentException("Unsupported model " + model);
57 | }
58 |
59 | requestCreator
60 | .skipMemoryCache()
61 | .transform(transformation)
62 | .into(imageView);
63 | }
64 |
65 | public static BitmapLoader createUsing(CropView cropView) {
66 | return createUsing(cropView, Picasso.with(cropView.getContext()));
67 | }
68 |
69 | public static BitmapLoader createUsing(CropView cropView, Picasso picasso) {
70 | return new PicassoBitmapLoader(picasso,
71 | PicassoFillViewportTransformation.createUsing(cropView.getViewportWidth(), cropView.getViewportHeight()));
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/PicassoFillViewportTransformation.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Lyft, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.lyft.android.scissors2;
17 |
18 | import android.graphics.Bitmap;
19 | import android.graphics.Rect;
20 | import com.squareup.picasso.Transformation;
21 |
22 | class PicassoFillViewportTransformation implements Transformation {
23 |
24 | private final int viewportWidth;
25 | private final int viewportHeight;
26 |
27 | public PicassoFillViewportTransformation(int viewportWidth, int viewportHeight) {
28 | this.viewportWidth = viewportWidth;
29 | this.viewportHeight = viewportHeight;
30 | }
31 |
32 | @Override
33 | public Bitmap transform(Bitmap source) {
34 | int sourceWidth = source.getWidth();
35 | int sourceHeight = source.getHeight();
36 |
37 | Rect target = CropViewExtensions.computeTargetSize(sourceWidth, sourceHeight, viewportWidth, viewportHeight);
38 | final Bitmap result = Bitmap.createScaledBitmap(
39 | source,
40 | target.width(),
41 | target.height(),
42 | true);
43 |
44 | if (result != source) {
45 | source.recycle();
46 | }
47 |
48 | return result;
49 | }
50 |
51 | @Override
52 | public String key() {
53 | return viewportWidth + "x" + viewportHeight;
54 | }
55 |
56 | public static Transformation createUsing(int viewportWidth, int viewportHeight) {
57 | return new PicassoFillViewportTransformation(viewportWidth, viewportHeight);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/TouchManager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Lyft, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.lyft.android.scissors2;
17 |
18 | import android.animation.Animator;
19 | import android.animation.AnimatorListenerAdapter;
20 | import android.animation.AnimatorSet;
21 | import android.animation.ValueAnimator;
22 | import android.annotation.TargetApi;
23 | import android.graphics.Matrix;
24 | import android.graphics.Rect;
25 | import android.os.Build;
26 | import android.support.annotation.IntDef;
27 | import android.view.GestureDetector;
28 | import android.view.MotionEvent;
29 | import android.view.ScaleGestureDetector;
30 | import android.view.animation.AccelerateDecelerateInterpolator;
31 | import android.view.animation.DecelerateInterpolator;
32 | import android.view.animation.Interpolator;
33 | import android.widget.ImageView;
34 | import android.widget.OverScroller;
35 |
36 | import java.lang.annotation.Retention;
37 | import java.lang.annotation.RetentionPolicy;
38 |
39 | class TouchManager {
40 |
41 | private static final int MINIMUM_FLING_VELOCITY = 2500;
42 |
43 | private final CropViewConfig cropViewConfig;
44 |
45 | private final ScaleGestureDetector scaleGestureDetector;
46 | private final GestureDetector gestureDetector;
47 |
48 | private float minimumScale;
49 | private float maximumScale;
50 | private Rect imageBounds;
51 | private float aspectRatio;
52 | private int viewportWidth;
53 | private int viewportHeight;
54 | private int bitmapWidth;
55 | private int bitmapHeight;
56 |
57 | private int verticalLimit;
58 | private int horizontalLimit;
59 |
60 | private float scale = -1.0f;
61 | private final TouchPoint position = new TouchPoint();
62 |
63 | private final ImageView imageView;
64 |
65 | private final GestureAnimator gestureAnimator = new GestureAnimator(new GestureAnimator.OnAnimationUpdateListener() {
66 | @Override
67 | public void onAnimationUpdate(@GestureAnimator.AnimationType int animationType, float animationValue) {
68 | if(animationType == GestureAnimator.ANIMATION_X) {
69 | position.set(animationValue, position.getY());
70 | ensureInsideViewport();
71 | }
72 | else if(animationType == GestureAnimator.ANIMATION_Y) {
73 | position.set(position.getX(), animationValue);
74 | ensureInsideViewport();
75 | }
76 | else if(animationType == GestureAnimator.ANIMATION_SCALE) {
77 | scale = animationValue;
78 | setLimits();
79 | }
80 |
81 | imageView.invalidate();
82 | }
83 |
84 | @Override
85 | public void onAnimationFinished() {
86 | ensureInsideViewport();
87 | }
88 | });
89 |
90 | private final ScaleGestureDetector.OnScaleGestureListener scaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() {
91 | @Override
92 | public boolean onScale(ScaleGestureDetector detector) {
93 | scale = calculateScale(detector.getScaleFactor());
94 | setLimits();
95 | return true;
96 | }
97 |
98 | @Override public boolean onScaleBegin(ScaleGestureDetector detector) {return true;}
99 | @Override public void onScaleEnd(ScaleGestureDetector detector) {}
100 | };
101 |
102 | private final GestureDetector.OnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() {
103 | @Override
104 | public boolean onDown(MotionEvent e) {
105 | return true;
106 | }
107 |
108 | @Override
109 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
110 | if (e2.getPointerCount() != 1) {
111 | return true;
112 | }
113 |
114 | TouchPoint delta = new TouchPoint(-distanceX, -distanceY);
115 | position.add(delta);
116 | ensureInsideViewport();
117 | return true;
118 | }
119 |
120 | @Override
121 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
122 | velocityX /= 2;
123 | velocityY /= 2;
124 |
125 | if(Math.abs(velocityX) < MINIMUM_FLING_VELOCITY) {
126 | velocityX = 0;
127 | }
128 | if(Math.abs(velocityY) < MINIMUM_FLING_VELOCITY) {
129 | velocityY = 0;
130 | }
131 |
132 | if(velocityX == 0 && velocityY == 0) {
133 | return true;
134 | }
135 |
136 | int width = (int) (imageBounds.right * scale);
137 | int height = (int) (imageBounds.bottom * scale);
138 |
139 | OverScroller scroller = new OverScroller(imageView.getContext());
140 | scroller.fling((int) e1.getX(), (int) e1.getY(), (int) velocityX, (int) velocityY, -width, width, -height, height);
141 |
142 | TouchPoint target = new TouchPoint(scroller.getFinalX(), scroller.getFinalY());
143 | float x = velocityX == 0 ? position.getX() : target.getX() * scale;
144 | float y = velocityY == 0 ? position.getY() : target.getY() * scale;
145 |
146 | gestureAnimator.animateTranslation(position.getX(), x, position.getY(), y);
147 |
148 | return true;
149 | }
150 |
151 | @Override
152 | public boolean onDoubleTap(MotionEvent e) {
153 | final float fromX, toX, fromY, toY, targetScale;
154 |
155 | TouchPoint eventPoint = new TouchPoint(e.getX(), e.getY());
156 | if(scale == minimumScale) {
157 | targetScale = maximumScale / 2;
158 | TouchPoint translatedTargetPosition = mapTouchCoordinateToMatrix(eventPoint, targetScale);
159 | TouchPoint centeredTargetPosition = centerCoordinates(translatedTargetPosition);
160 | fromX = position.getX();
161 | toX = centeredTargetPosition.getX();
162 | fromY = position.getY();
163 | toY = centeredTargetPosition.getY();
164 | }
165 | else {
166 | targetScale = minimumScale;
167 | TouchPoint translatedPosition = mapTouchCoordinateToMatrix(eventPoint, scale);
168 | TouchPoint centeredTargetPosition = centerCoordinates(translatedPosition);
169 | fromX = centeredTargetPosition.getX();
170 | toX = 0;
171 | fromY = centeredTargetPosition.getY();
172 | toY = 0;
173 | }
174 |
175 | gestureAnimator.animateDoubleTap(fromX, toX, fromY, toY, scale, targetScale);
176 | return true;
177 | }
178 |
179 | private TouchPoint centerCoordinates(TouchPoint coordinates) {
180 | float x = coordinates.getX() + (imageBounds.right / 2);
181 | float y = coordinates.getY() + (imageBounds.bottom / 2);
182 | return new TouchPoint(x, y);
183 | }
184 | };
185 |
186 | public TouchManager(final ImageView imageView, final CropViewConfig cropViewConfig) {
187 | this.imageView = imageView;
188 | scaleGestureDetector = new ScaleGestureDetector(imageView.getContext(), scaleGestureListener);
189 | gestureDetector = new GestureDetector(imageView.getContext(), gestureListener);
190 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
191 | scaleGestureDetector.setQuickScaleEnabled(true);
192 | }
193 |
194 | this.cropViewConfig = cropViewConfig;
195 |
196 | minimumScale = cropViewConfig.getMinScale();
197 | maximumScale = cropViewConfig.getMaxScale();
198 | }
199 |
200 | @TargetApi(Build.VERSION_CODES.FROYO)
201 | public void onEvent(MotionEvent event) {
202 | scaleGestureDetector.onTouchEvent(event);
203 | gestureDetector.onTouchEvent(event);
204 |
205 | if (isUpAction(event.getActionMasked())) {
206 | ensureInsideViewport();
207 | }
208 | }
209 |
210 | public void applyPositioningAndScale(Matrix matrix) {
211 | matrix.postTranslate(-bitmapWidth / 2.0f, -bitmapHeight / 2.0f);
212 | matrix.postScale(scale, scale);
213 | matrix.postTranslate(position.getX(), position.getY());
214 | }
215 |
216 | public void resetFor(int bitmapWidth, int bitmapHeight, int availableWidth, int availableHeight) {
217 | aspectRatio = cropViewConfig.getViewportRatio();
218 | imageBounds = new Rect(0, 0, availableWidth / 2, availableHeight / 2);
219 | setViewport(bitmapWidth, bitmapHeight, availableWidth, availableHeight);
220 |
221 | this.bitmapWidth = bitmapWidth;
222 | this.bitmapHeight = bitmapHeight;
223 | if (bitmapWidth > 0 && bitmapHeight > 0) {
224 | setMinimumScale();
225 | setLimits();
226 | resetPosition();
227 | ensureInsideViewport();
228 | }
229 | }
230 |
231 | public int getViewportWidth() {
232 | return viewportWidth;
233 | }
234 |
235 | public int getViewportHeight() {
236 | return viewportHeight;
237 | }
238 |
239 | public float getAspectRatio() {
240 | return aspectRatio;
241 | }
242 |
243 | public void setAspectRatio(float ratio) {
244 | aspectRatio = ratio;
245 | cropViewConfig.setViewportRatio(ratio);
246 | }
247 |
248 | private TouchPoint mapTouchCoordinateToMatrix(TouchPoint coordinate, float targetScale) {
249 | float width = bitmapWidth * targetScale;
250 | float height = bitmapHeight * targetScale;
251 |
252 | float x0 = width / 2;
253 | float y0 = height / 2;
254 |
255 | float newX = coordinate.getX() * targetScale;
256 | newX = -(newX - x0);
257 |
258 | float newY = coordinate.getY() * targetScale;
259 | if(newY > y0) {
260 | newY = -(newY - y0);
261 | }
262 | else {
263 | newY = y0 - newY;
264 | }
265 |
266 | return new TouchPoint(newX, newY);
267 | }
268 |
269 | private void ensureInsideViewport() {
270 | if (imageBounds == null) {
271 | return;
272 | }
273 |
274 | float newY = position.getY();
275 | int bottom = imageBounds.bottom;
276 |
277 |
278 | if (bottom - newY >= verticalLimit) {
279 | newY = bottom - verticalLimit;
280 | } else if (newY - bottom >= verticalLimit) {
281 | newY = bottom + verticalLimit;
282 | }
283 |
284 | float newX = position.getX();
285 | int right = imageBounds.right;
286 | if (newX <= right - horizontalLimit) {
287 | newX = right - horizontalLimit;
288 | } else if (newX > right + horizontalLimit) {
289 | newX = right + horizontalLimit;
290 | }
291 |
292 | position.set(newX, newY);
293 | }
294 |
295 | private void setViewport(int bitmapWidth, int bitmapHeight, int availableWidth, int availableHeight) {
296 | final float imageAspect = (float) bitmapWidth / bitmapHeight;
297 | final float viewAspect = (float) availableWidth / availableHeight;
298 |
299 | float ratio = cropViewConfig.getViewportRatio();
300 | if (Float.compare(0f, ratio) == 0) {
301 | // viewport ratio of 0 means match native ratio of bitmap
302 | ratio = imageAspect;
303 | }
304 |
305 | if (ratio > viewAspect) {
306 | // viewport is wider than view
307 | viewportWidth = availableWidth - cropViewConfig.getViewportOverlayPadding() * 2;
308 | viewportHeight = (int) (viewportWidth * (1 / ratio));
309 | } else {
310 | // viewport is taller than view
311 | viewportHeight = availableHeight - cropViewConfig.getViewportOverlayPadding() * 2;
312 | viewportWidth = (int) (viewportHeight * ratio);
313 | }
314 | }
315 |
316 | private void setLimits() {
317 | horizontalLimit = computeLimit((int) (bitmapWidth * scale), viewportWidth);
318 | verticalLimit = computeLimit((int) (bitmapHeight * scale), viewportHeight);
319 | }
320 |
321 | private void resetPosition() {
322 | position.set(imageBounds.right, imageBounds.bottom);
323 | }
324 |
325 | private void setMinimumScale() {
326 | final float fw = (float) viewportWidth / bitmapWidth;
327 | final float fh = (float) viewportHeight / bitmapHeight;
328 | minimumScale = Math.max(fw, fh);
329 | scale = Math.max(scale, minimumScale);
330 | }
331 |
332 | private float calculateScale(float newScaleDelta) {
333 | return Math.max(minimumScale, Math.min(scale * newScaleDelta, maximumScale));
334 | }
335 |
336 | private static int computeLimit(int bitmapSize, int viewportSize) {
337 | return (bitmapSize - viewportSize) / 2;
338 | }
339 |
340 | private static boolean isUpAction(int actionMasked) {
341 | return actionMasked == MotionEvent.ACTION_POINTER_UP || actionMasked == MotionEvent.ACTION_UP;
342 | }
343 |
344 | private static class GestureAnimator {
345 | @IntDef({ANIMATION_X, ANIMATION_Y, ANIMATION_SCALE})
346 | @Retention(RetentionPolicy.SOURCE)
347 | public @interface AnimationType {}
348 | public static final int ANIMATION_X = 0;
349 | public static final int ANIMATION_Y = 1;
350 | public static final int ANIMATION_SCALE = 2;
351 |
352 | interface OnAnimationUpdateListener {
353 | void onAnimationUpdate(@AnimationType int animationType, float animationValue);
354 | void onAnimationFinished();
355 | }
356 |
357 | private ValueAnimator xAnimator;
358 | private ValueAnimator yAnimator;
359 | private ValueAnimator scaleAnimator;
360 |
361 | private AnimatorSet animator;
362 |
363 | private final OnAnimationUpdateListener listener;
364 |
365 | public GestureAnimator(OnAnimationUpdateListener listener) {
366 | this.listener = listener;
367 | }
368 |
369 | final ValueAnimator.AnimatorUpdateListener updateListener = new ValueAnimator.AnimatorUpdateListener() {
370 | @Override
371 | public void onAnimationUpdate(ValueAnimator animation) {
372 | float val = ((float) animation.getAnimatedValue());
373 |
374 | if(animation == xAnimator) {
375 | listener.onAnimationUpdate(ANIMATION_X, val);
376 | }
377 | else if(animation == yAnimator) {
378 | listener.onAnimationUpdate(ANIMATION_Y, val);
379 | }
380 | else if(animation == scaleAnimator) {
381 | listener.onAnimationUpdate(ANIMATION_SCALE, val);
382 | }
383 | }
384 | };
385 |
386 | private final Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
387 | @Override
388 | public void onAnimationEnd(Animator animation) {
389 | if(xAnimator != null) xAnimator.removeUpdateListener(updateListener);
390 | if(yAnimator != null) yAnimator.removeUpdateListener(updateListener);
391 | if(scaleAnimator != null) scaleAnimator.removeUpdateListener(updateListener);
392 | animator.removeAllListeners();
393 | listener.onAnimationFinished();
394 | }
395 | };
396 |
397 | public void animateTranslation(float fromX, float toX, float fromY, float toY) {
398 | if(animator != null) {
399 | animator.cancel();
400 | }
401 |
402 | xAnimator = ValueAnimator.ofFloat(fromX, toX);
403 | yAnimator = ValueAnimator.ofFloat(fromY, toY);
404 | scaleAnimator = null;
405 |
406 | xAnimator.addUpdateListener(updateListener);
407 | yAnimator.addUpdateListener(updateListener);
408 |
409 | animate(new DecelerateInterpolator(), 250, xAnimator, yAnimator);
410 | }
411 |
412 | public void animateDoubleTap(float fromX, float toX, float fromY, float toY, float fromScale, float toScale) {
413 | if(animator != null) {
414 | animator.cancel();
415 | }
416 |
417 | xAnimator = ValueAnimator.ofFloat(fromX, toX);
418 | yAnimator = ValueAnimator.ofFloat(fromY, toY);
419 | scaleAnimator = ValueAnimator.ofFloat(fromScale, toScale);
420 |
421 | xAnimator.addUpdateListener(updateListener);
422 | yAnimator.addUpdateListener(updateListener);
423 | scaleAnimator.addUpdateListener(updateListener);
424 |
425 | animate(new AccelerateDecelerateInterpolator(), 500, scaleAnimator, xAnimator, yAnimator);
426 | }
427 |
428 | private void animate(Interpolator interpolator, long duration, ValueAnimator first, ValueAnimator... animators) {
429 | animator = new AnimatorSet();
430 | animator.setDuration(duration);
431 | animator.setInterpolator(interpolator);
432 | animator.addListener(animatorListener);
433 | AnimatorSet.Builder builder = animator.play(first);
434 | for(ValueAnimator valueAnimator : animators) {
435 | builder.with(valueAnimator);
436 | }
437 | animator.start();
438 | }
439 | }
440 | }
441 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/TouchPoint.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Lyft, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.lyft.android.scissors2;
17 |
18 | class TouchPoint {
19 |
20 | private float x;
21 | private float y;
22 |
23 | public TouchPoint() {
24 | }
25 |
26 | public TouchPoint(float x, float y) {
27 | this.x = x;
28 | this.y = y;
29 | }
30 |
31 | public float getX() {
32 | return x;
33 | }
34 |
35 | public float getY() {
36 | return y;
37 | }
38 |
39 | public float getLength() {
40 | return (float) Math.sqrt(x * x + y * y);
41 | }
42 |
43 | public TouchPoint copy(TouchPoint other) {
44 | x = other.getX();
45 | y = other.getY();
46 | return this;
47 | }
48 |
49 | public TouchPoint set(float x, float y) {
50 | this.x = x;
51 | this.y = y;
52 | return this;
53 | }
54 |
55 | public TouchPoint add(TouchPoint value) {
56 | this.x += value.getX();
57 | this.y += value.getY();
58 | return this;
59 | }
60 |
61 | public static TouchPoint subtract(TouchPoint lhs, TouchPoint rhs) {
62 | return new TouchPoint(lhs.x - rhs.x, lhs.y - rhs.y);
63 | }
64 |
65 | @Override
66 | public String toString() {
67 | return String.format("(%.4f, %.4f)", x, y);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/UILBitmapLoader.java:
--------------------------------------------------------------------------------
1 | package com.lyft.android.scissors2;
2 |
3 | import android.support.annotation.NonNull;
4 | import android.support.annotation.Nullable;
5 | import android.widget.ImageView;
6 |
7 | import com.nostra13.universalimageloader.core.DisplayImageOptions;
8 | import com.nostra13.universalimageloader.core.ImageLoader;
9 | import com.nostra13.universalimageloader.core.display.BitmapDisplayer;
10 |
11 | /**
12 | * A {@link BitmapLoader} with transformation for {@link ImageLoader} image library.
13 | *
14 | * @see UILBitmapLoader#createUsing(CropView)
15 | * @see UILBitmapLoader#createUsing(CropView, ImageLoader)
16 | */
17 | public class UILBitmapLoader implements BitmapLoader {
18 |
19 | private final ImageLoader imageLoader;
20 | private final BitmapDisplayer bitmapDisplayer;
21 |
22 | public UILBitmapLoader(ImageLoader imageLoader, BitmapDisplayer bitmapDisplayer) {
23 | this.imageLoader = imageLoader;
24 | this.bitmapDisplayer = bitmapDisplayer;
25 | }
26 |
27 | public static BitmapLoader createUsing(CropView cropView) {
28 | return createUsing(cropView, ImageLoader.getInstance());
29 | }
30 |
31 | public static BitmapLoader createUsing(CropView cropView, ImageLoader imageLoader) {
32 | return new UILBitmapLoader(imageLoader, UILFillViewportDisplayer.createUsing(cropView.getViewportWidth(), cropView.getViewportHeight()));
33 | }
34 |
35 | @Override
36 | public void load(@Nullable Object model, @NonNull ImageView view) {
37 | final DisplayImageOptions options = new DisplayImageOptions.Builder()
38 | .cacheInMemory(false)
39 | .cacheOnDisk(false)
40 | .displayer(bitmapDisplayer)
41 | .build();
42 |
43 | if (model instanceof String || model == null) {
44 | imageLoader.displayImage((String) model, view, options);
45 | } else {
46 | throw new IllegalArgumentException("Unsupported model " + model);
47 | }
48 |
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/UILFillViewportDisplayer.java:
--------------------------------------------------------------------------------
1 | package com.lyft.android.scissors2;
2 |
3 | import android.graphics.Bitmap;
4 | import android.graphics.Rect;
5 |
6 | import com.nostra13.universalimageloader.core.assist.LoadedFrom;
7 | import com.nostra13.universalimageloader.core.display.BitmapDisplayer;
8 | import com.nostra13.universalimageloader.core.imageaware.ImageAware;
9 |
10 | class UILFillViewportDisplayer implements BitmapDisplayer {
11 | private final int viewportWidth;
12 | private final int viewportHeight;
13 |
14 | public UILFillViewportDisplayer(int viewportWidth, int viewportHeight) {
15 | this.viewportWidth = viewportWidth;
16 | this.viewportHeight = viewportHeight;
17 | }
18 |
19 | public static BitmapDisplayer createUsing(int viewportWidth, int viewportHeight) {
20 | return new UILFillViewportDisplayer(viewportWidth, viewportHeight);
21 | }
22 |
23 | @Override
24 | public void display(Bitmap source, ImageAware imageAware, LoadedFrom loadedFrom) {
25 | int sourceWidth = source.getWidth();
26 | int sourceHeight = source.getHeight();
27 |
28 | Rect target = CropViewExtensions.computeTargetSize(sourceWidth, sourceHeight, viewportWidth, viewportHeight);
29 | final Bitmap result = Bitmap.createScaledBitmap(
30 | source,
31 | target.width(),
32 | target.height(),
33 | true);
34 |
35 | if (result != source) {
36 | source.recycle();
37 | }
38 |
39 | imageAware.setImageBitmap(result);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/scissors2/src/main/java/com/lyft/android/scissors2/Utils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Lyft, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.lyft.android.scissors2;
17 |
18 | import android.graphics.Bitmap;
19 | import android.graphics.Canvas;
20 | import android.graphics.Rect;
21 | import android.graphics.drawable.Drawable;
22 | import android.support.annotation.Nullable;
23 | import android.util.Log;
24 |
25 | import java.io.File;
26 | import java.io.FileOutputStream;
27 | import java.io.OutputStream;
28 | import java.util.concurrent.ExecutorService;
29 | import java.util.concurrent.Executors;
30 | import java.util.concurrent.Future;
31 |
32 | class Utils {
33 |
34 | public static void checkArg(boolean expression, String msg) {
35 | if (!expression) {
36 | throw new IllegalArgumentException(msg);
37 | }
38 | }
39 |
40 | public static void checkNotNull(Object object, String msg) {
41 | if (object == null) {
42 | throw new NullPointerException(msg);
43 | }
44 | }
45 |
46 | public static Bitmap asBitmap(Drawable drawable, int minWidth, int minHeight) {
47 | final Rect tmpRect = new Rect();
48 | drawable.copyBounds(tmpRect);
49 | if (tmpRect.isEmpty()) {
50 | tmpRect.set(0, 0, Math.max(minWidth, drawable.getIntrinsicWidth()), Math.max(minHeight, drawable.getIntrinsicHeight()));
51 | drawable.setBounds(tmpRect);
52 | }
53 | Bitmap bitmap = Bitmap.createBitmap(tmpRect.width(), tmpRect.height(), Bitmap.Config.ARGB_8888);
54 | drawable.draw(new Canvas(bitmap));
55 | return bitmap;
56 | }
57 |
58 | private final static ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();
59 | private static final String TAG = "scissors.Utils";
60 |
61 | public static Future flushToFile(final Bitmap bitmap,
62 | final Bitmap.CompressFormat format,
63 | final int quality,
64 | final File file) {
65 |
66 | return EXECUTOR_SERVICE.submit(new Runnable() {
67 | @Override
68 | public void run() {
69 | OutputStream outputStream = null;
70 |
71 | try {
72 | file.getParentFile().mkdirs();
73 | outputStream = new FileOutputStream(file);
74 | bitmap.compress(format, quality, outputStream);
75 | outputStream.flush();
76 | } catch (final Throwable throwable) {
77 | if (BuildConfig.DEBUG) {
78 | Log.e(TAG, "Error attempting to save bitmap.", throwable);
79 | }
80 | } finally {
81 | closeQuietly(outputStream);
82 | }
83 | }
84 | }, null);
85 | }
86 |
87 | public static Future flushToStream(final Bitmap bitmap,
88 | final Bitmap.CompressFormat format,
89 | final int quality,
90 | final OutputStream outputStream,
91 | final boolean closeWhenDone) {
92 |
93 | return EXECUTOR_SERVICE.submit(new Runnable() {
94 | @Override
95 | public void run() {
96 | try {
97 | bitmap.compress(format, quality, outputStream);
98 | outputStream.flush();
99 | } catch (final Throwable throwable) {
100 | if (BuildConfig.DEBUG) {
101 | Log.e(TAG, "Error attempting to save bitmap.", throwable);
102 | }
103 | } finally {
104 | if (closeWhenDone) {
105 | closeQuietly(outputStream);
106 | }
107 | }
108 | }
109 | }, null);
110 | }
111 |
112 | private static void closeQuietly(@Nullable OutputStream outputStream) {
113 | try {
114 | if (outputStream != null) {
115 | outputStream.close();
116 | }
117 | } catch (Exception e) {
118 | Log.e(TAG, "Error attempting to close stream.", e);
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/scissors2/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/scissors2/src/test/java/com/lyft/android/scissors2/TargetSizeTest.java:
--------------------------------------------------------------------------------
1 | package com.lyft.android.scissors2;
2 |
3 | import android.graphics.Rect;
4 | import java.util.Arrays;
5 | import java.util.Collection;
6 | import org.junit.Test;
7 | import org.junit.runner.RunWith;
8 | import org.robolectric.ParameterizedRobolectricTestRunner;
9 | import org.robolectric.annotation.Config;
10 |
11 | import static org.assertj.core.api.Assertions.assertThat;
12 | import static org.assertj.core.api.Assertions.fail;
13 |
14 | @RunWith(ParameterizedRobolectricTestRunner.class)
15 | @Config(manifest = Config.NONE)
16 | public class TargetSizeTest {
17 |
18 | @ParameterizedRobolectricTestRunner.Parameters(name = "{2} viewport = [{0}x{1}]")
19 | public static Collection