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 81 public CircleView(Context context) { 82 this(context, null /* attrs */); 83 } 84 85 public CircleView(Context context, AttributeSet attrs) { 86 this(context, attrs, 0 /* defStyleAttr */); 87 } 88 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 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 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 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 132 public boolean hasOverlappingRendering() { 133 // only if we have a background, which we shouldn't... 134 return getBackground() != null && getBackground().getCurrent() != null; 135 } 136 137 /** 138 * @return the current {@link Gravity} used to align/size the circle 139 */ 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 */ 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 */ 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 */ 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 * @return the x-coordinate of the center of the circle 192 */ 193 public final float getCenterX() { 194 return mCenterX; 195 } 196 197 /** 198 * Sets the x-coordinate for the center of the circle and invalidates only the affected area. 199 * 200 * @param centerX the x-coordinate to use, relative to the view's bounds 201 * @return this object, allowing calls to methods in this class to be chained 202 * @see R.styleable#CircleView_centerX 203 */ 204 public CircleView setCenterX(float centerX) { 205 final float oldCenterX = mCenterX; 206 if (oldCenterX != centerX) { 207 mCenterX = centerX; 208 209 // invalidate the old/new areas 210 invalidate(oldCenterX, mCenterY, mRadius); 211 invalidate(centerX, mCenterY, mRadius); 212 } 213 214 // clear the horizontal gravity flags 215 mGravity &= ~Gravity.HORIZONTAL_GRAVITY_MASK; 216 217 return this; 218 } 219 220 /** 221 * @return the y-coordinate of the center of the circle 222 */ 223 public final float getCenterY() { 224 return mCenterY; 225 } 226 227 /** 228 * Sets the y-coordinate for the center of the circle and invalidates only the affected area. 229 * 230 * @param centerY the y-coordinate to use, relative to the view's bounds 231 * @return this object, allowing calls to methods in this class to be chained 232 * @see R.styleable#CircleView_centerY 233 */ 234 public CircleView setCenterY(float centerY) { 235 final float oldCenterY = mCenterY; 236 if (oldCenterY != centerY) { 237 mCenterY = centerY; 238 239 // invalidate the old/new areas 240 invalidate(mCenterX, oldCenterY, mRadius); 241 invalidate(mCenterX, centerY, mRadius); 242 } 243 244 // clear the vertical gravity flags 245 mGravity &= ~Gravity.VERTICAL_GRAVITY_MASK; 246 247 return this; 248 } 249 250 /** 251 * @return the radius of the circle 252 */ 253 public final float getRadius() { 254 return mRadius; 255 } 256 257 /** 258 * Sets the radius of the circle and invalidates only the affected area. 259 * 260 * @param radius the radius to use 261 * @return this object, allowing calls to methods in this class to be chained 262 * @see R.styleable#CircleView_radius 263 */ 264 public CircleView setRadius(float radius) { 265 final float oldRadius = mRadius; 266 if (oldRadius != radius) { 267 mRadius = radius; 268 269 // invalidate the old/new areas 270 invalidate(mCenterX, mCenterY, oldRadius); 271 if (radius > oldRadius) { 272 invalidate(mCenterX, mCenterY, radius); 273 } 274 } 275 276 // clear the fill gravity flags 277 if ((mGravity & Gravity.FILL_HORIZONTAL) == Gravity.FILL_HORIZONTAL) { 278 mGravity &= ~Gravity.FILL_HORIZONTAL; 279 } 280 if ((mGravity & Gravity.FILL_VERTICAL) == Gravity.FILL_VERTICAL) { 281 mGravity &= ~Gravity.FILL_VERTICAL; 282 } 283 284 return this; 285 } 286 287 /** 288 * Invalidates the rectangular area that circumscribes the circle defined by {@code centerX}, 289 * {@code centerY}, and {@code radius}. 290 */ 291 private void invalidate(float centerX, float centerY, float radius) { 292 invalidate((int) (centerX - radius - 0.5f), (int) (centerY - radius - 0.5f), 293 (int) (centerX + radius + 0.5f), (int) (centerY + radius + 0.5f)); 294 } 295 296 /** 297 * Applies the specified {@code gravity} and {@code layoutDirection}, adjusting the alignment 298 * and size of the circle depending on the resolved {@link Gravity} flags. Also invalidates the 299 * affected area if necessary. 300 * 301 * @param gravity the {@link Gravity} the {@link Gravity} flags to use 302 * @param layoutDirection the layout direction used to resolve the absolute gravity 303 */ 304 @SuppressLint("RtlHardcoded") 305 private void applyGravity(int gravity, int layoutDirection) { 306 final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); 307 308 final float oldRadius = mRadius; 309 final float oldCenterX = mCenterX; 310 final float oldCenterY = mCenterY; 311 312 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 313 case Gravity.LEFT: 314 mCenterX = 0.0f; 315 break; 316 case Gravity.CENTER_HORIZONTAL: 317 case Gravity.FILL_HORIZONTAL: 318 mCenterX = getWidth() / 2.0f; 319 break; 320 case Gravity.RIGHT: 321 mCenterX = getWidth(); 322 break; 323 } 324 325 switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { 326 case Gravity.TOP: 327 mCenterY = 0.0f; 328 break; 329 case Gravity.CENTER_VERTICAL: 330 case Gravity.FILL_VERTICAL: 331 mCenterY = getHeight() / 2.0f; 332 break; 333 case Gravity.BOTTOM: 334 mCenterY = getHeight(); 335 break; 336 } 337 338 switch (absoluteGravity & Gravity.FILL) { 339 case Gravity.FILL: 340 mRadius = Math.min(getWidth(), getHeight()) / 2.0f; 341 break; 342 case Gravity.FILL_HORIZONTAL: 343 mRadius = getWidth() / 2.0f; 344 break; 345 case Gravity.FILL_VERTICAL: 346 mRadius = getHeight() / 2.0f; 347 break; 348 } 349 350 if (oldCenterX != mCenterX || oldCenterY != mCenterY || oldRadius != mRadius) { 351 invalidate(oldCenterX, oldCenterY, oldRadius); 352 invalidate(mCenterX, mCenterY, mRadius); 353 } 354 } 355 } 356