ImageShow.java revision 24d6ec7beb37eb3a5449f1fa49b4adb123391d24
1/*
2 * Copyright (C) 2012 The Android Open Source Project
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
17package com.android.gallery3d.filtershow.imageshow;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.Matrix;
25import android.graphics.Paint;
26import android.graphics.Point;
27import android.graphics.Rect;
28import android.graphics.RectF;
29import android.util.AttributeSet;
30import android.view.GestureDetector;
31import android.view.GestureDetector.OnDoubleTapListener;
32import android.view.GestureDetector.OnGestureListener;
33import android.view.MotionEvent;
34import android.view.ScaleGestureDetector;
35import android.view.View;
36import android.widget.LinearLayout;
37
38import com.android.gallery3d.R;
39import com.android.gallery3d.filtershow.FilterShowActivity;
40import com.android.gallery3d.filtershow.cache.ImageLoader;
41import com.android.gallery3d.filtershow.filters.ImageFilter;
42import com.android.gallery3d.filtershow.pipeline.ImagePreset;
43import com.android.gallery3d.filtershow.tools.SaveImage;
44
45import java.io.File;
46
47public class ImageShow extends View implements OnGestureListener,
48        ScaleGestureDetector.OnScaleGestureListener,
49        OnDoubleTapListener {
50
51    private static final String LOGTAG = "ImageShow";
52    private static final boolean ENABLE_ZOOMED_COMPARISON = false;
53
54    protected Paint mPaint = new Paint();
55    protected int mTextSize;
56    protected int mTextPadding;
57
58    protected ImageLoader mImageLoader = null;
59
60    protected int mBackgroundColor;
61
62    private GestureDetector mGestureDetector = null;
63    private ScaleGestureDetector mScaleGestureDetector = null;
64
65    protected Rect mImageBounds = new Rect();
66    private boolean mOriginalDisabled = false;
67    private boolean mTouchShowOriginal = false;
68    private long mTouchShowOriginalDate = 0;
69    private final long mTouchShowOriginalDelayMin = 200; // 200ms
70    private int mShowOriginalDirection = 0;
71    private static int UNVEIL_HORIZONTAL = 1;
72    private static int UNVEIL_VERTICAL = 2;
73
74    private Point mTouchDown = new Point();
75    private Point mTouch = new Point();
76    private boolean mFinishedScalingOperation = false;
77
78    private int mOriginalTextMargin;
79    private int mOriginalTextSize;
80    private String mOriginalText;
81    private boolean mZoomIn = false;
82    Point mOriginalTranslation = new Point();
83    float mOriginalScale;
84    float mStartFocusX, mStartFocusY;
85    private enum InteractionMode {
86        NONE,
87        SCALE,
88        MOVE
89    }
90    InteractionMode mInteractionMode = InteractionMode.NONE;
91
92    protected GeometryMetadata getGeometry() {
93        return new GeometryMetadata(getImagePreset().getGeometry());
94    }
95
96    private FilterShowActivity mActivity = null;
97
98    public FilterShowActivity getActivity() {
99        return mActivity;
100    }
101
102    public boolean hasModifications() {
103        return MasterImage.getImage().hasModifications();
104    }
105
106    public void resetParameter() {
107        // TODO: implement reset
108    }
109
110    public void onNewValue(int parameter) {
111        invalidate();
112    }
113
114    public ImageShow(Context context, AttributeSet attrs, int defStyle) {
115        super(context, attrs, defStyle);
116        setupImageShow(context);
117    }
118
119    public ImageShow(Context context, AttributeSet attrs) {
120        super(context, attrs);
121        setupImageShow(context);
122
123    }
124
125    public ImageShow(Context context) {
126        super(context);
127        setupImageShow(context);
128    }
129
130    private void setupImageShow(Context context) {
131        Resources res = context.getResources();
132        mTextSize = res.getDimensionPixelSize(R.dimen.photoeditor_text_size);
133        mTextPadding = res.getDimensionPixelSize(R.dimen.photoeditor_text_padding);
134        mOriginalTextMargin = res.getDimensionPixelSize(R.dimen.photoeditor_original_text_margin);
135        mOriginalTextSize = res.getDimensionPixelSize(R.dimen.photoeditor_original_text_size);
136        mBackgroundColor = res.getColor(R.color.background_screen);
137        mOriginalText = res.getString(R.string.original_picture_text);
138        setupGestureDetector(context);
139        mActivity = (FilterShowActivity) context;
140        MasterImage.getImage().addObserver(this);
141    }
142
143    public void setupGestureDetector(Context context) {
144        mGestureDetector = new GestureDetector(context, this);
145        mScaleGestureDetector = new ScaleGestureDetector(context, this);
146    }
147
148    @Override
149    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
150        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
151        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
152        setMeasuredDimension(parentWidth, parentHeight);
153    }
154
155    public ImageFilter getCurrentFilter() {
156        return MasterImage.getImage().getCurrentFilter();
157    }
158
159    public Rect getImageBounds() {
160        Rect dst = new Rect();
161        getImagePreset().getGeometry().getPhotoBounds().roundOut(dst);
162        return dst;
163    }
164
165    public Rect getImageCropBounds() {
166        return GeometryMath.roundNearest(getImagePreset().getGeometry().getPreviewCropBounds());
167    }
168
169    /* consider moving the following 2 methods into a subclass */
170    /**
171     * This function calculates a Image to Screen Transformation matrix
172     *
173     * @param reflectRotation set true if you want the rotation encoded
174     * @return Image to Screen transformation matrix
175     */
176    protected Matrix getImageToScreenMatrix(boolean reflectRotation) {
177        GeometryMetadata geo = getImagePreset().getGeometry();
178        if (geo == null || mImageLoader == null
179                || MasterImage.getImage().getOriginalBounds() == null) {
180            return new Matrix();
181        }
182        Matrix m = geo.getOriginalToScreen(reflectRotation,
183                MasterImage.getImage().getOriginalBounds().width(),
184                MasterImage.getImage().getOriginalBounds().height(), getWidth(), getHeight());
185        Point translate = MasterImage.getImage().getTranslation();
186        float scaleFactor = MasterImage.getImage().getScaleFactor();
187        m.postTranslate(translate.x, translate.y);
188        m.postScale(scaleFactor, scaleFactor, getWidth() / 2.0f, getHeight() / 2.0f);
189        return m;
190    }
191
192    /**
193     * This function calculates a to Screen Image Transformation matrix
194     *
195     * @param reflectRotation set true if you want the rotation encoded
196     * @return Screen to Image transformation matrix
197     */
198    protected Matrix getScreenToImageMatrix(boolean reflectRotation) {
199        Matrix m = getImageToScreenMatrix(reflectRotation);
200        Matrix invert = new Matrix();
201        m.invert(invert);
202        return invert;
203    }
204
205    public ImagePreset getImagePreset() {
206        return MasterImage.getImage().getPreset();
207    }
208
209    @Override
210    public void onDraw(Canvas canvas) {
211        MasterImage.getImage().setImageShowSize(getWidth(), getHeight());
212
213        float cx = canvas.getWidth()/2.0f;
214        float cy = canvas.getHeight()/2.0f;
215        float scaleFactor = MasterImage.getImage().getScaleFactor();
216        Point translation = MasterImage.getImage().getTranslation();
217
218        Matrix scalingMatrix = new Matrix();
219        scalingMatrix.postScale(scaleFactor, scaleFactor, cx, cy);
220        scalingMatrix.preTranslate(translation.x, translation.y);
221
222        RectF unscaledClipRect = new RectF(mImageBounds);
223        scalingMatrix.mapRect(unscaledClipRect, unscaledClipRect);
224
225        canvas.save();
226
227        boolean enablePartialRendering = false;
228
229        // For now, partial rendering is disabled for all filters,
230        // so no need to clip.
231        if (enablePartialRendering && !unscaledClipRect.isEmpty()) {
232            canvas.clipRect(unscaledClipRect);
233        }
234
235        canvas.save();
236        // TODO: center scale on gesture
237        canvas.scale(scaleFactor, scaleFactor, cx, cy);
238        canvas.translate(translation.x, translation.y);
239        drawImage(canvas, getFilteredImage(), true);
240        Bitmap highresPreview = MasterImage.getImage().getHighresImage();
241        if (highresPreview != null) {
242            drawImage(canvas, highresPreview, false);
243        }
244        canvas.restore();
245
246        Bitmap partialPreview = MasterImage.getImage().getPartialImage();
247        if (partialPreview != null) {
248            Rect src = new Rect(0, 0, partialPreview.getWidth(), partialPreview.getHeight());
249            Rect dest = new Rect(0, 0, getWidth(), getHeight());
250            canvas.drawBitmap(partialPreview, src, dest, mPaint);
251        }
252
253        canvas.save();
254        canvas.scale(scaleFactor, scaleFactor, cx, cy);
255        canvas.translate(translation.x, translation.y);
256        drawPartialImage(canvas, getGeometryOnlyImage());
257        canvas.restore();
258
259        canvas.restore();
260    }
261
262    public void resetImageCaches(ImageShow caller) {
263        if (mImageLoader == null) {
264            return;
265        }
266        MasterImage.getImage().updatePresets(true);
267    }
268
269    public Bitmap getFiltersOnlyImage() {
270        return MasterImage.getImage().getFiltersOnlyImage();
271    }
272
273    public Bitmap getGeometryOnlyImage() {
274        return MasterImage.getImage().getGeometryOnlyImage();
275    }
276
277    public Bitmap getFilteredImage() {
278        return MasterImage.getImage().getFilteredImage();
279    }
280
281    public void drawImage(Canvas canvas, Bitmap image, boolean updateBounds) {
282        if (image != null) {
283            Rect s = new Rect(0, 0, image.getWidth(),
284                    image.getHeight());
285
286            float scale = GeometryMath.scale(image.getWidth(), image.getHeight(), getWidth(),
287                    getHeight());
288
289            float w = image.getWidth() * scale;
290            float h = image.getHeight() * scale;
291            float ty = (getHeight() - h) / 2.0f;
292            float tx = (getWidth() - w) / 2.0f;
293
294            Rect d = new Rect((int) tx, (int) ty, (int) (w + tx),
295                    (int) (h + ty));
296            if (updateBounds) {
297                mImageBounds = d;
298            }
299            canvas.drawBitmap(image, s, d, mPaint);
300        }
301    }
302
303    public void drawPartialImage(Canvas canvas, Bitmap image) {
304        boolean showsOriginal = MasterImage.getImage().showsOriginal();
305        if (!showsOriginal && !mTouchShowOriginal)
306            return;
307        canvas.save();
308        if (image != null) {
309            if (mShowOriginalDirection == 0) {
310                if (Math.abs(mTouch.y - mTouchDown.y) > Math.abs(mTouch.x - mTouchDown.x)) {
311                    mShowOriginalDirection = UNVEIL_VERTICAL;
312                } else {
313                    mShowOriginalDirection = UNVEIL_HORIZONTAL;
314                }
315            }
316
317            int px = 0;
318            int py = 0;
319            if (mShowOriginalDirection == UNVEIL_VERTICAL) {
320                px = mImageBounds.width();
321                py = mTouch.y - mImageBounds.top;
322            } else {
323                px = mTouch.x - mImageBounds.left;
324                py = mImageBounds.height();
325                if (showsOriginal) {
326                    px = mImageBounds.width();
327                }
328            }
329
330            Rect d = new Rect(mImageBounds.left, mImageBounds.top,
331                    mImageBounds.left + px, mImageBounds.top + py);
332            canvas.clipRect(d);
333            drawImage(canvas, image, false);
334            Paint paint = new Paint();
335            paint.setColor(Color.BLACK);
336            paint.setStrokeWidth(3);
337
338            if (mShowOriginalDirection == UNVEIL_VERTICAL) {
339                canvas.drawLine(mImageBounds.left, mTouch.y,
340                        mImageBounds.right, mTouch.y, paint);
341            } else {
342                canvas.drawLine(mTouch.x, mImageBounds.top,
343                        mTouch.x, mImageBounds.bottom, paint);
344            }
345
346            Rect bounds = new Rect();
347            paint.setAntiAlias(true);
348            paint.setTextSize(mOriginalTextSize);
349            paint.getTextBounds(mOriginalText, 0, mOriginalText.length(), bounds);
350            paint.setColor(Color.BLACK);
351            paint.setStyle(Paint.Style.STROKE);
352            paint.setStrokeWidth(3);
353            canvas.drawText(mOriginalText, mImageBounds.left + mOriginalTextMargin,
354                    mImageBounds.top + bounds.height() + mOriginalTextMargin, paint);
355            paint.setStyle(Paint.Style.FILL);
356            paint.setStrokeWidth(1);
357            paint.setColor(Color.WHITE);
358            canvas.drawText(mOriginalText, mImageBounds.left + mOriginalTextMargin,
359                    mImageBounds.top + bounds.height() + mOriginalTextMargin, paint);
360        }
361        canvas.restore();
362    }
363
364    public void bindAsImageLoadListener() {
365        MasterImage.getImage().addListener(this);
366    }
367
368    private void imageSizeChanged(Bitmap image) {
369        if (image == null || getImagePreset() == null)
370            return;
371        float w = image.getWidth();
372        float h = image.getHeight();
373        GeometryMetadata geo = getImagePreset().getGeometry();
374        RectF pb = geo.getPhotoBounds();
375        if (w == pb.width() && h == pb.height()) {
376            return;
377        }
378        RectF r = new RectF(0, 0, w, h);
379        geo.setPhotoBounds(r);
380        geo.setCropBounds(r);
381        getImagePreset().setGeometry(geo);
382    }
383
384    public void updateImage() {
385        invalidate();
386        Bitmap bitmap = MasterImage.getImage().getOriginalBitmapLarge();
387        if (bitmap != null) {
388            imageSizeChanged(bitmap);
389        }
390    }
391
392    public void imageLoaded() {
393        updateImage();
394        invalidate();
395    }
396
397    public void saveImage(FilterShowActivity filterShowActivity, File file) {
398        SaveImage.saveImage(getImagePreset(), filterShowActivity, file);
399    }
400
401
402    public boolean scaleInProgress() {
403        return mScaleGestureDetector.isInProgress();
404    }
405
406    @Override
407    public boolean onTouchEvent(MotionEvent event) {
408        super.onTouchEvent(event);
409        int action = event.getAction();
410        action = action & MotionEvent.ACTION_MASK;
411
412        mGestureDetector.onTouchEvent(event);
413        boolean scaleInProgress = scaleInProgress();
414        mScaleGestureDetector.onTouchEvent(event);
415        if (mInteractionMode == InteractionMode.SCALE) {
416            return true;
417        }
418        if (!scaleInProgress() && scaleInProgress) {
419            // If we were scaling, the scale will stop but we will
420            // still issue an ACTION_UP. Let the subclasses know.
421            mFinishedScalingOperation = true;
422        }
423
424        int ex = (int) event.getX();
425        int ey = (int) event.getY();
426        if (action == MotionEvent.ACTION_DOWN) {
427            mInteractionMode = InteractionMode.MOVE;
428            mTouchDown.x = ex;
429            mTouchDown.y = ey;
430            mTouchShowOriginalDate = System.currentTimeMillis();
431            mShowOriginalDirection = 0;
432            MasterImage.getImage().setOriginalTranslation(MasterImage.getImage().getTranslation());
433        }
434
435        if (action == MotionEvent.ACTION_MOVE && mInteractionMode == InteractionMode.MOVE) {
436            mTouch.x = ex;
437            mTouch.y = ey;
438
439            float scaleFactor = MasterImage.getImage().getScaleFactor();
440            if (scaleFactor > 1 && (!ENABLE_ZOOMED_COMPARISON || event.getPointerCount() == 2)) {
441                float translateX = (mTouch.x - mTouchDown.x) / scaleFactor;
442                float translateY = (mTouch.y - mTouchDown.y) / scaleFactor;
443                Point originalTranslation = MasterImage.getImage().getOriginalTranslation();
444                Point translation = MasterImage.getImage().getTranslation();
445                translation.x = (int) (originalTranslation.x + translateX);
446                translation.y = (int) (originalTranslation.y + translateY);
447                constrainTranslation(translation, scaleFactor);
448                MasterImage.getImage().setTranslation(translation);
449                mTouchShowOriginal = false;
450            } else if (enableComparison() && !mOriginalDisabled
451                    && (System.currentTimeMillis() - mTouchShowOriginalDate
452                            > mTouchShowOriginalDelayMin)
453                    && event.getPointerCount() == 1) {
454                mTouchShowOriginal = true;
455            }
456        }
457
458        if (action == MotionEvent.ACTION_UP) {
459            mInteractionMode = InteractionMode.NONE;
460            mTouchShowOriginal = false;
461            mTouchDown.x = 0;
462            mTouchDown.y = 0;
463            mTouch.x = 0;
464            mTouch.y = 0;
465            if (MasterImage.getImage().getScaleFactor() <= 1) {
466                MasterImage.getImage().setScaleFactor(1);
467                MasterImage.getImage().resetTranslation();
468            }
469        }
470        invalidate();
471        return true;
472    }
473
474    protected boolean enableComparison() {
475        return true;
476    }
477
478    @Override
479    public boolean onDoubleTap(MotionEvent arg0) {
480        mZoomIn = !mZoomIn;
481        float scale = 1.0f;
482        if (mZoomIn) {
483            scale = MasterImage.getImage().getMaxScaleFactor();
484        }
485        if (scale != MasterImage.getImage().getScaleFactor()) {
486            MasterImage.getImage().setScaleFactor(scale);
487            float translateX = (getWidth() / 2 - arg0.getX());
488            float translateY = (getHeight() / 2 - arg0.getY());
489            Point translation = MasterImage.getImage().getTranslation();
490            translation.x = (int) (mOriginalTranslation.x + translateX);
491            translation.y = (int) (mOriginalTranslation.y + translateY);
492            constrainTranslation(translation, scale);
493            MasterImage.getImage().setTranslation(translation);
494            invalidate();
495        }
496        return true;
497    }
498
499    private void constrainTranslation(Point translation, float scale) {
500        float maxTranslationX = getWidth() / scale;
501        float maxTranslationY = getHeight() / scale;
502        if (Math.abs(translation.x) > maxTranslationX) {
503            translation.x = (int) (Math.signum(translation.x) *
504                    maxTranslationX);
505            if (Math.abs(translation.y) > maxTranslationY) {
506                translation.y = (int) (Math.signum(translation.y) *
507                        maxTranslationY);
508            }
509
510        }
511    }
512
513    @Override
514    public boolean onDoubleTapEvent(MotionEvent arg0) {
515        return false;
516    }
517
518    @Override
519    public boolean onSingleTapConfirmed(MotionEvent arg0) {
520        return false;
521    }
522
523    @Override
524    public boolean onDown(MotionEvent arg0) {
525        return false;
526    }
527
528    @Override
529    public boolean onFling(MotionEvent startEvent, MotionEvent endEvent, float arg2, float arg3) {
530        if (mActivity == null) {
531            return false;
532        }
533        if (endEvent.getPointerCount() == 2) {
534            return false;
535        }
536        return true;
537    }
538
539    @Override
540    public void onLongPress(MotionEvent arg0) {
541    }
542
543    @Override
544    public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) {
545        return false;
546    }
547
548    @Override
549    public void onShowPress(MotionEvent arg0) {
550    }
551
552    @Override
553    public boolean onSingleTapUp(MotionEvent arg0) {
554        return false;
555    }
556
557    public boolean useUtilityPanel() {
558        return false;
559    }
560
561    public void openUtilityPanel(final LinearLayout accessoryViewList) {
562    }
563
564    @Override
565    public boolean onScale(ScaleGestureDetector detector) {
566        MasterImage img = MasterImage.getImage();
567        float scaleFactor = img.getScaleFactor();
568
569        scaleFactor = scaleFactor * detector.getScaleFactor();
570        if (scaleFactor > MasterImage.getImage().getMaxScaleFactor()) {
571            scaleFactor = MasterImage.getImage().getMaxScaleFactor();
572        }
573        if (scaleFactor < 0.5) {
574            scaleFactor = 0.5f;
575        }
576        MasterImage.getImage().setScaleFactor(scaleFactor);
577        scaleFactor = img.getScaleFactor();
578        float focusx = detector.getFocusX();
579        float focusy = detector.getFocusY();
580        float translateX = (focusx - mStartFocusX) / scaleFactor;
581        float translateY = (focusy - mStartFocusY) / scaleFactor;
582        Point translation = MasterImage.getImage().getTranslation();
583        translation.x = (int) (mOriginalTranslation.x + translateX);
584        translation.y = (int) (mOriginalTranslation.y + translateY);
585        constrainTranslation(translation, scaleFactor);
586        MasterImage.getImage().setTranslation(translation);
587
588        invalidate();
589        return true;
590    }
591
592    @Override
593    public boolean onScaleBegin(ScaleGestureDetector detector) {
594        Point pos = MasterImage.getImage().getTranslation();
595        mOriginalTranslation.x = pos.x;
596        mOriginalTranslation.y = pos.y;
597        mOriginalScale = MasterImage.getImage().getScaleFactor();
598        mStartFocusX = detector.getFocusX();
599        mStartFocusY = detector.getFocusY();
600        mInteractionMode = InteractionMode.SCALE;
601        return true;
602    }
603
604    @Override
605    public void onScaleEnd(ScaleGestureDetector detector) {
606        mInteractionMode = InteractionMode.NONE;
607        if (MasterImage.getImage().getScaleFactor() < 1) {
608            MasterImage.getImage().setScaleFactor(1);
609            invalidate();
610        }
611    }
612
613    public boolean didFinishScalingOperation() {
614        if (mFinishedScalingOperation) {
615            mFinishedScalingOperation = false;
616            return true;
617        }
618        return false;
619    }
620
621}
622