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