1 /* 2 * Copyright (C) 2015 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.deskclock.widget; 18 19 import android.annotation.SuppressLint; 20 import android.content.Context; 21 import android.content.res.TypedArray; 22 import android.graphics.Canvas; 23 import android.graphics.Color; 24 import android.graphics.Paint; 25 import android.util.AttributeSet; 26 import android.util.Property; 27 import android.view.Gravity; 28 import android.view.View; 29 30 import com.android.deskclock.R; 31 32 /** 33 * A {@link View} that draws primitive circles. 34 */ 35 public class CircleView extends View { 36 37 /** 38 * A Property wrapper around the fillColor functionality handled by the 39 * {@link #setFillColor(int)} and {@link #getFillColor()} methods. 40 */ 41 public final static Property<CircleView, Integer> FILL_COLOR = 42 new Property<CircleView, Integer>(Integer.class, "fillColor") { 43 @Override 44 public Integer get(CircleView view) { 45 return view.getFillColor(); 46 } 47 48 @Override 49 public void set(CircleView view, Integer value) { 50 view.setFillColor(value); 51 } 52 }; 53 54 /** 55 * A Property wrapper around the radius functionality handled by the 56 * {@link #setRadius(float)} and {@link #getRadius()} methods. 57 */ 58 public final static Property<CircleView, Float> RADIUS = 59 new Property<CircleView, Float>(Float.class, "radius") { 60 @Override 61 public Float get(CircleView view) { 62 return view.getRadius(); 63 } 64 65 @Override 66 public void set(CircleView view, Float value) { 67 view.setRadius(value); 68 } 69 }; 70 71 /** 72 * The {@link Paint} used to draw the circle. 73 */ 74 private final Paint mCirclePaint = new Paint(); 75 76 private int mGravity; 77 private float mCenterX; 78 private float mCenterY; 79 private float mRadius; 80 CircleView(Context context)81 public CircleView(Context context) { 82 this(context, null /* attrs */); 83 } 84 CircleView(Context context, AttributeSet attrs)85 public CircleView(Context context, AttributeSet attrs) { 86 this(context, attrs, 0 /* defStyleAttr */); 87 } 88 CircleView(Context context, AttributeSet attrs, int defStyleAttr)89 public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { 90 super(context, attrs, defStyleAttr); 91 92 final TypedArray a = context.obtainStyledAttributes( 93 attrs, R.styleable.CircleView, defStyleAttr, 0 /* defStyleRes */); 94 95 mGravity = a.getInt(R.styleable.CircleView_android_gravity, Gravity.NO_GRAVITY); 96 mCenterX = a.getDimension(R.styleable.CircleView_centerX, 0.0f); 97 mCenterY = a.getDimension(R.styleable.CircleView_centerY, 0.0f); 98 mRadius = a.getDimension(R.styleable.CircleView_radius, 0.0f); 99 100 mCirclePaint.setColor(a.getColor(R.styleable.CircleView_fillColor, Color.WHITE)); 101 102 a.recycle(); 103 } 104 105 @Override onRtlPropertiesChanged(int layoutDirection)106 public void onRtlPropertiesChanged(int layoutDirection) { 107 super.onRtlPropertiesChanged(layoutDirection); 108 109 if (mGravity != Gravity.NO_GRAVITY) { 110 applyGravity(mGravity, layoutDirection); 111 } 112 } 113 114 @Override onLayout(boolean changed, int left, int top, int right, int bottom)115 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 116 super.onLayout(changed, left, top, right, bottom); 117 118 if (mGravity != Gravity.NO_GRAVITY) { 119 applyGravity(mGravity, getLayoutDirection()); 120 } 121 } 122 123 @Override onDraw(Canvas canvas)124 protected void onDraw(Canvas canvas) { 125 super.onDraw(canvas); 126 127 // draw the circle, duh 128 canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint); 129 } 130 131 @Override hasOverlappingRendering()132 public boolean hasOverlappingRendering() { 133 // only if we have a background, which we shouldn't... 134 return getBackground() != null; 135 } 136 137 /** 138 * @return the current {@link Gravity} used to align/size the circle 139 */ getGravity()140 public final int getGravity() { 141 return mGravity; 142 } 143 144 /** 145 * Describes how to align/size the circle relative to the view's bounds. Defaults to 146 * {@link Gravity#NO_GRAVITY}. 147 * <p/> 148 * Note: using {@link #setCenterX(float)}, {@link #setCenterY(float)}, or 149 * {@link #setRadius(float)} will automatically clear any conflicting gravity bits. 150 * 151 * @param gravity the {@link Gravity} flags to use 152 * @return this object, allowing calls to methods in this class to be chained 153 * @see R.styleable#CircleView_android_gravity 154 */ setGravity(int gravity)155 public CircleView setGravity(int gravity) { 156 if (mGravity != gravity) { 157 mGravity = gravity; 158 159 if (gravity != Gravity.NO_GRAVITY && isLayoutDirectionResolved()) { 160 applyGravity(gravity, getLayoutDirection()); 161 } 162 } 163 return this; 164 } 165 166 /** 167 * @return the ARGB color used to fill the circle 168 */ getFillColor()169 public final int getFillColor() { 170 return mCirclePaint.getColor(); 171 } 172 173 /** 174 * Sets the ARGB color used to fill the circle and invalidates only the affected area. 175 * 176 * @param color the ARGB color to use 177 * @return this object, allowing calls to methods in this class to be chained 178 * @see R.styleable#CircleView_fillColor 179 */ setFillColor(int color)180 public CircleView setFillColor(int color) { 181 if (mCirclePaint.getColor() != color) { 182 mCirclePaint.setColor(color); 183 184 // invalidate the current area 185 invalidate(mCenterX, mCenterY, mRadius); 186 } 187 return this; 188 } 189 190 /** 191 * Sets the x-coordinate for the center of the circle and invalidates only the affected area. 192 * 193 * @param centerX the x-coordinate to use, relative to the view's bounds 194 * @return this object, allowing calls to methods in this class to be chained 195 * @see R.styleable#CircleView_centerX 196 */ setCenterX(float centerX)197 public CircleView setCenterX(float centerX) { 198 final float oldCenterX = mCenterX; 199 if (oldCenterX != centerX) { 200 mCenterX = centerX; 201 202 // invalidate the old/new areas 203 invalidate(oldCenterX, mCenterY, mRadius); 204 invalidate(centerX, mCenterY, mRadius); 205 } 206 207 // clear the horizontal gravity flags 208 mGravity &= ~Gravity.HORIZONTAL_GRAVITY_MASK; 209 210 return this; 211 } 212 213 /** 214 * Sets the y-coordinate for the center of the circle and invalidates only the affected area. 215 * 216 * @param centerY the y-coordinate to use, relative to the view's bounds 217 * @return this object, allowing calls to methods in this class to be chained 218 * @see R.styleable#CircleView_centerY 219 */ setCenterY(float centerY)220 public CircleView setCenterY(float centerY) { 221 final float oldCenterY = mCenterY; 222 if (oldCenterY != centerY) { 223 mCenterY = centerY; 224 225 // invalidate the old/new areas 226 invalidate(mCenterX, oldCenterY, mRadius); 227 invalidate(mCenterX, centerY, mRadius); 228 } 229 230 // clear the vertical gravity flags 231 mGravity &= ~Gravity.VERTICAL_GRAVITY_MASK; 232 233 return this; 234 } 235 236 /** 237 * @return the radius of the circle 238 */ getRadius()239 public final float getRadius() { 240 return mRadius; 241 } 242 243 /** 244 * Sets the radius of the circle and invalidates only the affected area. 245 * 246 * @param radius the radius to use 247 * @return this object, allowing calls to methods in this class to be chained 248 * @see R.styleable#CircleView_radius 249 */ setRadius(float radius)250 public CircleView setRadius(float radius) { 251 final float oldRadius = mRadius; 252 if (oldRadius != radius) { 253 mRadius = radius; 254 255 // invalidate the old/new areas 256 invalidate(mCenterX, mCenterY, oldRadius); 257 if (radius > oldRadius) { 258 invalidate(mCenterX, mCenterY, radius); 259 } 260 } 261 262 // clear the fill gravity flags 263 if ((mGravity & Gravity.FILL_HORIZONTAL) == Gravity.FILL_HORIZONTAL) { 264 mGravity &= ~Gravity.FILL_HORIZONTAL; 265 } 266 if ((mGravity & Gravity.FILL_VERTICAL) == Gravity.FILL_VERTICAL) { 267 mGravity &= ~Gravity.FILL_VERTICAL; 268 } 269 270 return this; 271 } 272 273 /** 274 * Invalidates the rectangular area that circumscribes the circle defined by {@code centerX}, 275 * {@code centerY}, and {@code radius}. 276 */ invalidate(float centerX, float centerY, float radius)277 private void invalidate(float centerX, float centerY, float radius) { 278 invalidate((int) (centerX - radius - 0.5f), (int) (centerY - radius - 0.5f), 279 (int) (centerX + radius + 0.5f), (int) (centerY + radius + 0.5f)); 280 } 281 282 /** 283 * Applies the specified {@code gravity} and {@code layoutDirection}, adjusting the alignment 284 * and size of the circle depending on the resolved {@link Gravity} flags. Also invalidates the 285 * affected area if necessary. 286 * 287 * @param gravity the {@link Gravity} the {@link Gravity} flags to use 288 * @param layoutDirection the layout direction used to resolve the absolute gravity 289 */ 290 @SuppressLint("RtlHardcoded") applyGravity(int gravity, int layoutDirection)291 private void applyGravity(int gravity, int layoutDirection) { 292 final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); 293 294 final float oldRadius = mRadius; 295 final float oldCenterX = mCenterX; 296 final float oldCenterY = mCenterY; 297 298 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 299 case Gravity.LEFT: 300 mCenterX = 0.0f; 301 break; 302 case Gravity.CENTER_HORIZONTAL: 303 case Gravity.FILL_HORIZONTAL: 304 mCenterX = getWidth() / 2.0f; 305 break; 306 case Gravity.RIGHT: 307 mCenterX = getWidth(); 308 break; 309 } 310 311 switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { 312 case Gravity.TOP: 313 mCenterY = 0.0f; 314 break; 315 case Gravity.CENTER_VERTICAL: 316 case Gravity.FILL_VERTICAL: 317 mCenterY = getHeight() / 2.0f; 318 break; 319 case Gravity.BOTTOM: 320 mCenterY = getHeight(); 321 break; 322 } 323 324 switch (absoluteGravity & Gravity.FILL) { 325 case Gravity.FILL: 326 mRadius = Math.min(getWidth(), getHeight()) / 2.0f; 327 break; 328 case Gravity.FILL_HORIZONTAL: 329 mRadius = getWidth() / 2.0f; 330 break; 331 case Gravity.FILL_VERTICAL: 332 mRadius = getHeight() / 2.0f; 333 break; 334 } 335 336 if (oldCenterX != mCenterX || oldCenterY != mCenterY || oldRadius != mRadius) { 337 invalidate(oldCenterX, oldCenterY, oldRadius); 338 invalidate(mCenterX, mCenterY, mRadius); 339 } 340 } 341 } 342