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 && getBackground().getCurrent() != 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      * @return the x-coordinate of the center of the circle
192      */
getCenterX()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      */
setCenterX(float centerX)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      */
getCenterY()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      */
setCenterY(float centerY)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      */
getRadius()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      */
setRadius(float radius)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      */
invalidate(float centerX, float centerY, float radius)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")
applyGravity(int gravity, int layoutDirection)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