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.content.res.Resources; 21 import android.graphics.Canvas; 22 import android.graphics.Paint; 23 import android.graphics.drawable.Drawable; 24 import android.util.AttributeSet; 25 import android.view.MotionEvent; 26 import android.view.View; 27 import android.view.animation.AnimationUtils; 28 29 import com.android.photoeditor.R; 30 31 /** 32 * Wheel that has a draggable thumb to set and get the normalized scale value from 0 to 1. 33 */ 34 class ScaleWheel extends View { 35 36 /** 37 * Listens to scale changes. 38 */ 39 public interface OnScaleChangeListener { 40 onProgressChanged(float progress, boolean fromUser)41 void onProgressChanged(float progress, boolean fromUser); 42 } 43 44 private static final float MATH_PI = (float) Math.PI; 45 private static final float MATH_HALF_PI = MATH_PI / 2; 46 47 // Angles are defined between PI and -PI. 48 private static final float ANGLE_SPANNED = MATH_PI * 4 / 3; 49 private static final float ANGLE_BEGIN = ANGLE_SPANNED / 2.0f; 50 51 private static final float THUMB_RADIUS_RATIO = 0.363f; 52 private static final float INNER_RADIUS_RATIO = 0.24f; 53 54 private final Drawable thumb; 55 private final Drawable background; 56 private final int thumbSize; 57 private final Paint circlePaint; 58 private final int maxProgress; 59 private final float radiantInterval; 60 private int thumbRadius; 61 private int innerRadius; 62 private int centerXY; 63 private float angle; 64 private int progress; 65 private boolean dragThumb; 66 private OnScaleChangeListener listener; 67 ScaleWheel(Context context, AttributeSet attrs)68 public ScaleWheel(Context context, AttributeSet attrs) { 69 super(context, attrs); 70 71 Resources resources = context.getResources(); 72 thumbSize = (int) resources.getDimension(R.dimen.wheel_thumb_size); 73 74 // Set the maximum progress and compute the radiant interval between progress values. 75 maxProgress = 100; 76 radiantInterval = ANGLE_SPANNED / maxProgress; 77 78 thumb = resources.getDrawable(R.drawable.wheel_knot_selector); 79 background = resources.getDrawable(R.drawable.scale_wheel_background); 80 background.setAlpha(160); 81 82 circlePaint = new Paint(); 83 circlePaint.setAntiAlias(true); 84 circlePaint.setColor(resources.getColor(R.color.scale_wheel_interior_color)); 85 } 86 setProgress(float progress)87 public void setProgress(float progress) { 88 if (updateProgress((int) (progress * maxProgress), false)) { 89 updateThumbPositionByProgress(); 90 } 91 } 92 setOnScaleChangeListener(OnScaleChangeListener listener)93 public void setOnScaleChangeListener(OnScaleChangeListener listener) { 94 this.listener = listener; 95 } 96 97 @Override setVisibility(int visibility)98 public void setVisibility(int visibility) { 99 super.setVisibility(visibility); 100 101 startAnimation(AnimationUtils.loadAnimation(getContext(), 102 (visibility == VISIBLE) ? R.anim.wheel_show : R.anim.wheel_hide)); 103 } 104 105 @Override onSizeChanged(int w, int h, int oldw, int oldh)106 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 107 super.onSizeChanged(w, h, oldw, oldh); 108 109 int wheelSize = Math.min(w, h); 110 thumbRadius = (int) (wheelSize * THUMB_RADIUS_RATIO); 111 innerRadius = (int) (wheelSize * INNER_RADIUS_RATIO); 112 113 // The wheel would be centered at (centerXY, centerXY) and have outer-radius centerXY. 114 centerXY = wheelSize / 2; 115 updateThumbPositionByProgress(); 116 } 117 118 @Override onDraw(Canvas canvas)119 protected void onDraw(Canvas canvas) { 120 super.onDraw(canvas); 121 122 background.setBounds(0, 0, getWidth(), getHeight()); 123 background.draw(canvas); 124 125 int thumbX = (int) (thumbRadius * Math.cos(angle) + centerXY); 126 int thumbY = (int) (centerXY - thumbRadius * Math.sin(angle)); 127 int halfSize = thumbSize / 2; 128 thumb.setBounds(thumbX - halfSize, thumbY - halfSize, thumbX + halfSize, thumbY + halfSize); 129 thumb.draw(canvas); 130 131 float radius = (progress * innerRadius) / maxProgress; 132 canvas.drawCircle(centerXY, centerXY, radius, circlePaint); 133 } 134 135 @Override onTouchEvent(MotionEvent ev)136 public boolean onTouchEvent(MotionEvent ev) { 137 super.onTouchEvent(ev); 138 139 if (isEnabled()) { 140 switch (ev.getAction()) { 141 case MotionEvent.ACTION_DOWN: 142 updateThumbState( 143 isHittingThumbArea(ev.getX() - centerXY, centerXY - ev.getY())); 144 break; 145 146 case MotionEvent.ACTION_MOVE: 147 float x = ev.getX() - centerXY; 148 float y = centerXY - ev.getY(); 149 if (!dragThumb && !updateThumbState(isHittingThumbArea(x, y))) { 150 // The thumb wasn't dragged and isn't being dragged, either. 151 break; 152 } 153 154 if (updateAngle(x, y)) { 155 int progress = (int) ((ANGLE_BEGIN - angle) / radiantInterval); 156 if (updateProgress(progress, true)) { 157 invalidate(); 158 } 159 } 160 break; 161 162 case MotionEvent.ACTION_CANCEL: 163 case MotionEvent.ACTION_UP: 164 updateThumbState(false); 165 break; 166 } 167 } 168 return true; 169 } 170 isHittingThumbArea(float x, float y)171 private boolean isHittingThumbArea(float x, float y) { 172 double radius = Math.sqrt((x * x) + (y * y)); 173 return (radius > innerRadius) && (radius < centerXY); 174 } 175 updateAngle(float x, float y)176 private boolean updateAngle(float x, float y) { 177 float angle; 178 if (x == 0) { 179 angle = (y >= 0) ? MATH_HALF_PI : -MATH_HALF_PI; 180 } else { 181 angle = (float) Math.atan(y / x); 182 } 183 184 if ((angle >= 0) && (x < 0)) { 185 angle = angle - MATH_PI; 186 } else if ((angle < 0) && (x < 0)) { 187 angle = MATH_PI + angle; 188 } 189 190 if ((angle > ANGLE_BEGIN) || (angle <= ANGLE_BEGIN - ANGLE_SPANNED)) { 191 return false; 192 } 193 194 this.angle = angle; 195 return true; 196 } 197 updateProgress(int progress, boolean fromUser)198 private boolean updateProgress(int progress, boolean fromUser) { 199 if ((this.progress != progress) && (progress >= 0) && (progress <= maxProgress)) { 200 this.progress = progress; 201 202 if (listener != null) { 203 listener.onProgressChanged((float) progress / maxProgress, fromUser); 204 } 205 return true; 206 } 207 return false; 208 } 209 updateThumbPositionByProgress()210 private void updateThumbPositionByProgress() { 211 angle = ANGLE_BEGIN - progress * radiantInterval; 212 invalidate(); 213 } 214 updateThumbState(boolean dragThumb)215 private boolean updateThumbState(boolean dragThumb) { 216 if (this.dragThumb == dragThumb) { 217 // The state hasn't been changed; no need for updates. 218 return false; 219 } 220 221 this.dragThumb = dragThumb; 222 thumb.setState(dragThumb ? PRESSED_ENABLED_STATE_SET : ENABLED_STATE_SET); 223 invalidate(); 224 return true; 225 } 226 } 227