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