Home | History | Annotate | Download | only in actions
      1 /*
      2  * Copyright (C) 2010 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 
     17 package com.android.gallery3d.photoeditor.actions;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.graphics.Canvas;
     22 import android.graphics.Paint;
     23 import android.graphics.RectF;
     24 import android.graphics.drawable.Drawable;
     25 import android.util.AttributeSet;
     26 import android.view.MotionEvent;
     27 
     28 import com.android.gallery3d.R;
     29 
     30 /**
     31  * A view that tracks touch motions and adjusts crop bounds accordingly.
     32  */
     33 class CropView extends FullscreenToolView {
     34 
     35     /**
     36      * Listener of crop bounds.
     37      */
     38     public interface OnCropChangeListener {
     39 
     40         void onCropChanged(RectF cropBounds, boolean fromUser);
     41     }
     42 
     43     private static final int MOVE_LEFT = 1;
     44     private static final int MOVE_TOP = 2;
     45     private static final int MOVE_RIGHT = 4;
     46     private static final int MOVE_BOTTOM = 8;
     47     private static final int MOVE_BLOCK = 16;
     48 
     49     private static final int MIN_CROP_WIDTH_HEIGHT = 2;
     50     private static final int TOUCH_TOLERANCE = 25;
     51     private static final int SHADOW_ALPHA = 160;
     52 
     53     private final Paint borderPaint;
     54     private final Drawable cropIndicator;
     55     private final int indicatorSize;
     56     private final RectF cropBounds = new RectF(0, 0, 1, 1);
     57 
     58     private float lastX;
     59     private float lastY;
     60     private int movingEdges;
     61     private OnCropChangeListener listener;
     62 
     63     public CropView(Context context, AttributeSet attrs) {
     64         super(context, attrs);
     65 
     66         Resources resources = context.getResources();
     67         cropIndicator = resources.getDrawable(R.drawable.camera_crop_holo);
     68         indicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size);
     69         int borderColor = resources.getColor(R.color.opaque_cyan);
     70 
     71         borderPaint = new Paint();
     72         borderPaint.setStyle(Paint.Style.STROKE);
     73         borderPaint.setColor(borderColor);
     74         borderPaint.setStrokeWidth(2f);
     75     }
     76 
     77     public void setOnCropChangeListener(OnCropChangeListener listener) {
     78         this.listener = listener;
     79     }
     80 
     81     private void refreshByCropChange(boolean fromUser) {
     82         if (listener != null) {
     83             listener.onCropChanged(new RectF(cropBounds), fromUser);
     84         }
     85         invalidate();
     86     }
     87 
     88     /**
     89      * Sets cropped bounds; modifies the bounds if it's smaller than the allowed dimensions.
     90      */
     91     public void setCropBounds(RectF bounds) {
     92         // Avoid cropping smaller than minimum width or height.
     93         if (bounds.width() * getPhotoWidth() < MIN_CROP_WIDTH_HEIGHT) {
     94             bounds.set(0, bounds.top, 1, bounds.bottom);
     95         }
     96         if (bounds.height() * getPhotoHeight() < MIN_CROP_WIDTH_HEIGHT) {
     97             bounds.set(bounds.left, 0, bounds.right, 1);
     98         }
     99         cropBounds.set(bounds);
    100         refreshByCropChange(false);
    101     }
    102 
    103     private RectF getCropBoundsDisplayed() {
    104         float width = displayBounds.width();
    105         float height = displayBounds.height();
    106         RectF cropped = new RectF(cropBounds.left * width, cropBounds.top * height,
    107                 cropBounds.right * width, cropBounds.bottom * height);
    108         cropped.offset(displayBounds.left, displayBounds.top);
    109         return cropped;
    110     }
    111 
    112     private void detectMovingEdges(float x, float y) {
    113         RectF cropped = getCropBoundsDisplayed();
    114         movingEdges = 0;
    115 
    116         // Check left or right.
    117         float left = Math.abs(x - cropped.left);
    118         float right = Math.abs(x - cropped.right);
    119         if ((left <= TOUCH_TOLERANCE) && (left < right)) {
    120             movingEdges |= MOVE_LEFT;
    121         }
    122         else if (right <= TOUCH_TOLERANCE) {
    123             movingEdges |= MOVE_RIGHT;
    124         }
    125 
    126         // Check top or bottom.
    127         float top = Math.abs(y - cropped.top);
    128         float bottom = Math.abs(y - cropped.bottom);
    129         if ((top <= TOUCH_TOLERANCE) & (top < bottom)) {
    130             movingEdges |= MOVE_TOP;
    131         }
    132         else if (bottom <= TOUCH_TOLERANCE) {
    133             movingEdges |= MOVE_BOTTOM;
    134         }
    135 
    136         // Check inside block.
    137         if (cropped.contains(x, y) && (movingEdges == 0)) {
    138             movingEdges = MOVE_BLOCK;
    139         }
    140         invalidate();
    141     }
    142 
    143     private void moveEdges(float deltaX, float deltaY) {
    144         RectF cropped = getCropBoundsDisplayed();
    145         if (movingEdges == MOVE_BLOCK) {
    146             // Move the whole cropped bounds within the photo display bounds.
    147             deltaX = (deltaX > 0) ? Math.min(displayBounds.right - cropped.right, deltaX)
    148                     : Math.max(displayBounds.left - cropped.left, deltaX);
    149             deltaY = (deltaY > 0) ? Math.min(displayBounds.bottom - cropped.bottom, deltaY)
    150                     : Math.max(displayBounds.top - cropped.top, deltaY);
    151             cropped.offset(deltaX, deltaY);
    152         } else {
    153             // Adjust cropped bound dimensions within the photo display bounds.
    154             float minWidth = MIN_CROP_WIDTH_HEIGHT * displayBounds.width() / getPhotoWidth();
    155             float minHeight = MIN_CROP_WIDTH_HEIGHT * displayBounds.height() / getPhotoHeight();
    156             if ((movingEdges & MOVE_LEFT) != 0) {
    157                 cropped.left = Math.min(cropped.left + deltaX, cropped.right - minWidth);
    158             }
    159             if ((movingEdges & MOVE_TOP) != 0) {
    160                 cropped.top = Math.min(cropped.top + deltaY, cropped.bottom - minHeight);
    161             }
    162             if ((movingEdges & MOVE_RIGHT) != 0) {
    163                 cropped.right = Math.max(cropped.right + deltaX, cropped.left + minWidth);
    164             }
    165             if ((movingEdges & MOVE_BOTTOM) != 0) {
    166                 cropped.bottom = Math.max(cropped.bottom + deltaY, cropped.top + minHeight);
    167             }
    168             cropped.intersect(displayBounds);
    169         }
    170         mapPhotoRect(cropped, cropBounds);
    171         refreshByCropChange(true);
    172     }
    173 
    174     @Override
    175     public boolean onTouchEvent(MotionEvent event) {
    176         super.onTouchEvent(event);
    177 
    178         if (isEnabled()) {
    179             float x = event.getX();
    180             float y = event.getY();
    181 
    182             switch (event.getAction()) {
    183                 case MotionEvent.ACTION_DOWN:
    184                     detectMovingEdges(x, y);
    185                     lastX = x;
    186                     lastY = y;
    187                     break;
    188 
    189                 case MotionEvent.ACTION_MOVE:
    190                     if (movingEdges != 0) {
    191                         moveEdges(x - lastX, y - lastY);
    192                     }
    193                     lastX = x;
    194                     lastY = y;
    195                     break;
    196 
    197                 case MotionEvent.ACTION_CANCEL:
    198                 case MotionEvent.ACTION_UP:
    199                     movingEdges = 0;
    200                     invalidate();
    201                     break;
    202             }
    203         }
    204         return true;
    205     }
    206 
    207     private void drawIndicator(Canvas canvas, Drawable indicator, float centerX, float centerY) {
    208         int left = (int) centerX - indicatorSize / 2;
    209         int top = (int) centerY - indicatorSize / 2;
    210         indicator.setBounds(left, top, left + indicatorSize, top + indicatorSize);
    211         indicator.draw(canvas);
    212     }
    213 
    214     private void drawShadow(Canvas canvas, float left, float top, float right, float bottom) {
    215         canvas.save();
    216         canvas.clipRect(left, top, right, bottom);
    217         canvas.drawARGB(SHADOW_ALPHA, 0, 0, 0);
    218         canvas.restore();
    219     }
    220 
    221     @Override
    222     protected void onDraw(Canvas canvas) {
    223         super.onDraw(canvas);
    224 
    225         // Draw shadow on non-cropped bounds and the border around cropped bounds.
    226         RectF cropped = getCropBoundsDisplayed();
    227         drawShadow(canvas, displayBounds.left, displayBounds.top, displayBounds.right, cropped.top);
    228         drawShadow(canvas, displayBounds.left, cropped.top, cropped.left, displayBounds.bottom);
    229         drawShadow(canvas, cropped.right, cropped.top, displayBounds.right, displayBounds.bottom);
    230         drawShadow(canvas, cropped.left, cropped.bottom, cropped.right, displayBounds.bottom);
    231         canvas.drawRect(cropped, borderPaint);
    232 
    233         boolean notMoving = movingEdges == 0;
    234         if (((movingEdges & MOVE_TOP) != 0) || notMoving) {
    235             drawIndicator(canvas, cropIndicator, cropped.centerX(), cropped.top);
    236         }
    237         if (((movingEdges & MOVE_BOTTOM) != 0) || notMoving) {
    238             drawIndicator(canvas, cropIndicator, cropped.centerX(), cropped.bottom);
    239         }
    240         if (((movingEdges & MOVE_LEFT) != 0) || notMoving) {
    241             drawIndicator(canvas, cropIndicator, cropped.left, cropped.centerY());
    242         }
    243         if (((movingEdges & MOVE_RIGHT) != 0) || notMoving) {
    244             drawIndicator(canvas, cropIndicator, cropped.right, cropped.centerY());
    245         }
    246     }
    247 }
    248