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
17package com.android.deskclock.widget;
18
19import android.annotation.SuppressLint;
20import android.content.Context;
21import android.content.res.TypedArray;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.Paint;
25import android.util.AttributeSet;
26import android.util.Property;
27import android.view.Gravity;
28import android.view.View;
29
30import com.android.deskclock.R;
31
32/**
33 * A {@link View} that draws primitive circles.
34 */
35public 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;
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     * 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     */
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     */
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     */
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     */
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     */
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")
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