• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
onAngleChanged(float degrees, boolean fromUser)39         void onAngleChanged(float degrees, boolean fromUser);
40 
onStartTrackingTouch()41         void onStartTrackingTouch();
42 
onStopTrackingTouch()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 
RotateView(Context context, AttributeSet attrs)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 
setRotatedAngle(float degrees)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      */
setRotateSpan(float degrees)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      */
setGridBounds(RectF bounds)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 
setOnAngleChangeListener(OnRotateChangeListener listener)123     public void setOnAngleChangeListener(OnRotateChangeListener listener) {
124         this.listener = listener;
125     }
126 
127     @Override
onSizeChanged(int w, int h, int oldw, int oldh)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
onDraw(Canvas canvas)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 
calculateAngle(MotionEvent ev)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
onTouchEvent(MotionEvent ev)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 
notifyAngleChange(boolean fromUser)217     private void notifyAngleChange(boolean fromUser) {
218         if (listener != null) {
219             listener.onAngleChanged(-currentRotatedAngle * RADIAN_TO_DEGREE, fromUser);
220         }
221     }
222 }
223