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