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