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