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.photoeditor.actions;
     18 
     19 import android.content.Context;
     20 import android.graphics.Canvas;
     21 import android.graphics.DashPathEffect;
     22 import android.graphics.Paint;
     23 import android.graphics.Path;
     24 import android.graphics.RectF;
     25 import android.util.AttributeSet;
     26 import android.view.MotionEvent;
     27 import android.view.View;
     28 
     29 /**
     30  * View that shows grids and handles touch-events to adjust angle of rotation.
     31  */
     32 class RotateView extends View {
     33 
     34     /**
     35      * Listens to rotate changes.
     36      */
     37     public interface OnRotateChangeListener {
     38 
     39         void onAngleChanged(float degrees, boolean fromUser);
     40 
     41         void onStartTrackingTouch();
     42 
     43         void onStopTrackingTouch();
     44     }
     45 
     46     // All angles used are defined between PI and -PI.
     47     private static final float MATH_PI = (float) Math.PI;
     48     private static final float MATH_HALF_PI = MATH_PI / 2;
     49     private static final float RADIAN_TO_DEGREE = 180f / MATH_PI;
     50 
     51     private final Paint dashStrokePaint;
     52     private final Path grids = new Path();
     53     private final Path referenceLine = new Path();
     54     private final RectF referenceLineBounds = new RectF();
     55 
     56     private OnRotateChangeListener listener;
     57     private int centerX;
     58     private int centerY;
     59     private float maxRotatedAngle;
     60     private float minRotatedAngle;
     61     private float currentRotatedAngle;
     62     private float lastRotatedAngle;
     63     private float touchStartAngle;
     64 
     65     public RotateView(Context context, AttributeSet attrs) {
     66         super(context, attrs);
     67 
     68         dashStrokePaint = new Paint();
     69         dashStrokePaint.setAntiAlias(true);
     70         dashStrokePaint.setStyle(Paint.Style.STROKE);
     71         dashStrokePaint.setPathEffect(new DashPathEffect(new float[] {15.0f, 5.0f}, 1.0f));
     72     }
     73 
     74     public void setRotatedAngle(float degrees) {
     75         currentRotatedAngle = -degrees / RADIAN_TO_DEGREE;
     76         notifyAngleChange(false);
     77     }
     78 
     79     /**
     80      * Sets allowed degrees for rotation span before rotating the view.
     81      */
     82     public void setRotateSpan(float degrees) {
     83         if (degrees >= 360f) {
     84             maxRotatedAngle = Float.POSITIVE_INFINITY;
     85         } else {
     86             maxRotatedAngle = (degrees / RADIAN_TO_DEGREE) / 2;
     87         }
     88         minRotatedAngle = -maxRotatedAngle;
     89     }
     90 
     91     /**
     92      * Sets grid bounds to be drawn or null to hide grids right before the view is visible.
     93      */
     94     public void setGridBounds(RectF bounds) {
     95         grids.reset();
     96         referenceLine.reset();
     97         if (bounds != null) {
     98             float delta = bounds.width() / 4.0f;
     99             for (float x = bounds.left + delta; x < bounds.right; x += delta) {
    100                 grids.moveTo(x, bounds.top);
    101                 grids.lineTo(x, bounds.bottom);
    102             }
    103             delta = bounds.height() / 4.0f;
    104             for (float y = bounds.top + delta; y < bounds.bottom; y += delta) {
    105                 grids.moveTo(bounds.left, y);
    106                 grids.lineTo(bounds.right, y);
    107             }
    108 
    109             // Make reference line long enough to cross the bounds diagonally after being rotated.
    110             referenceLineBounds.set(bounds);
    111             float radius = (float) Math.hypot(centerX, centerY);
    112             delta = radius - centerX;
    113             referenceLine.moveTo(-delta, centerY);
    114             referenceLine.lineTo(getWidth() + delta, centerY);
    115 
    116             delta = radius - centerY;
    117             referenceLine.moveTo(centerX, -delta);
    118             referenceLine.lineTo(centerX, getHeight() + delta);
    119         }
    120         invalidate();
    121     }
    122 
    123     public void setOnAngleChangeListener(OnRotateChangeListener listener) {
    124         this.listener = listener;
    125     }
    126 
    127     @Override
    128     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    129         super.onSizeChanged(w, h, oldw, oldh);
    130 
    131         centerX = w / 2;
    132         centerY = h / 2;
    133     }
    134 
    135     @Override
    136     protected void onDraw(Canvas canvas) {
    137         super.onDraw(canvas);
    138 
    139         if (!grids.isEmpty()) {
    140             dashStrokePaint.setStrokeWidth(2f);
    141             dashStrokePaint.setColor(0x99CCCCCC);
    142             canvas.drawPath(grids, dashStrokePaint);
    143         }
    144 
    145         if (!referenceLine.isEmpty()) {
    146             dashStrokePaint.setStrokeWidth(2f);
    147             dashStrokePaint.setColor(0x99FFCC77);
    148             canvas.save();
    149             canvas.clipRect(referenceLineBounds);
    150             canvas.rotate(-currentRotatedAngle * RADIAN_TO_DEGREE, centerX, centerY);
    151             canvas.drawPath(referenceLine, dashStrokePaint);
    152             canvas.restore();
    153         }
    154     }
    155 
    156     private float calculateAngle(MotionEvent ev) {
    157         float x = ev.getX() - centerX;
    158         float y = centerY - ev.getY();
    159 
    160         float angle;
    161         if (x == 0) {
    162             angle = (y >= 0) ? MATH_HALF_PI : -MATH_HALF_PI;
    163         } else {
    164             angle = (float) Math.atan(y / x);
    165         }
    166 
    167         if ((angle >= 0) && (x < 0)) {
    168             angle = angle - MATH_PI;
    169         } else if ((angle < 0) && (x < 0)) {
    170             angle = MATH_PI + angle;
    171         }
    172         return angle;
    173     }
    174 
    175     @Override
    176     public boolean onTouchEvent(MotionEvent ev) {
    177         super.onTouchEvent(ev);
    178 
    179         if (isEnabled()) {
    180             switch (ev.getAction()) {
    181                 case MotionEvent.ACTION_DOWN:
    182                     lastRotatedAngle = currentRotatedAngle;
    183                     touchStartAngle = calculateAngle(ev);
    184 
    185                     if (listener != null) {
    186                         listener.onStartTrackingTouch();
    187                     }
    188                     break;
    189 
    190                 case MotionEvent.ACTION_MOVE:
    191                     float touchAngle = calculateAngle(ev);
    192                     float rotatedAngle = touchAngle - touchStartAngle + lastRotatedAngle;
    193 
    194                     if ((rotatedAngle > maxRotatedAngle) || (rotatedAngle < minRotatedAngle)) {
    195                         // Angles are out of range; restart rotating.
    196                         // TODO: Fix discontinuity around boundary.
    197                         lastRotatedAngle = currentRotatedAngle;
    198                         touchStartAngle = touchAngle;
    199                     } else {
    200                         currentRotatedAngle = rotatedAngle;
    201                         notifyAngleChange(true);
    202                         invalidate();
    203                     }
    204                     break;
    205 
    206                 case MotionEvent.ACTION_CANCEL:
    207                 case MotionEvent.ACTION_UP:
    208                     if (listener != null) {
    209                         listener.onStopTrackingTouch();
    210                     }
    211                     break;
    212             }
    213         }
    214         return true;
    215     }
    216 
    217     private void notifyAngleChange(boolean fromUser) {
    218         if (listener != null) {
    219             listener.onAngleChanged(-currentRotatedAngle * RADIAN_TO_DEGREE, fromUser);
    220         }
    221     }
    222 }
    223