1 /*
2  * Copyright (C) 2020 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 package com.android.deskclock.widget
17 
18 import android.annotation.SuppressLint
19 import android.content.Context
20 import android.graphics.Canvas
21 import android.graphics.Color
22 import android.graphics.Paint
23 import android.util.AttributeSet
24 import android.util.Property
25 import android.view.Gravity
26 import android.view.View
27 
28 import com.android.deskclock.R
29 
30 import kotlin.math.min
31 
32 /**
33  * A [View] that draws primitive circles.
34  */
35 class CircleView @JvmOverloads constructor(
36     context: Context,
37     attrs: AttributeSet? = null,
38     defStyleAttr: Int = 0
39 ) : View(context, attrs, defStyleAttr) {
40     /** The [Paint] used to draw the circle. */
41     private val mCirclePaint = Paint()
42 
43     /** the current [Gravity] used to align/size the circle */
44     var gravity: Int
45         private set
46 
47     private var mCenterX: Float
48     private var mCenterY: Float
49 
50     /** the radius of the circle */
51     var radius: Float
52         private set
53 
54     init {
55         val a = context.obtainStyledAttributes(attrs, R.styleable.CircleView, defStyleAttr, 0)
56 
57         gravity = a.getInt(R.styleable.CircleView_android_gravity, Gravity.NO_GRAVITY)
58         mCenterX = a.getDimension(R.styleable.CircleView_centerX, 0.0f)
59         mCenterY = a.getDimension(R.styleable.CircleView_centerY, 0.0f)
60         radius = a.getDimension(R.styleable.CircleView_radius, 0.0f)
61 
62         mCirclePaint.color = a.getColor(R.styleable.CircleView_fillColor, Color.WHITE)
63 
64         a.recycle()
65     }
66 
onRtlPropertiesChangednull67     override fun onRtlPropertiesChanged(layoutDirection: Int) {
68         super.onRtlPropertiesChanged(layoutDirection)
69 
70         if (gravity != Gravity.NO_GRAVITY) {
71             applyGravity(gravity, layoutDirection)
72         }
73     }
74 
onLayoutnull75     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
76         super.onLayout(changed, left, top, right, bottom)
77 
78         if (gravity != Gravity.NO_GRAVITY) {
79             applyGravity(gravity, layoutDirection)
80         }
81     }
82 
onDrawnull83     override fun onDraw(canvas: Canvas) {
84         super.onDraw(canvas)
85 
86         // draw the circle, duh
87         canvas.drawCircle(mCenterX, mCenterY, radius, mCirclePaint)
88     }
89 
hasOverlappingRenderingnull90     override fun hasOverlappingRendering(): Boolean {
91         // only if we have a background, which we shouldn't...
92         return background != null
93     }
94 
95     /**
96      * Describes how to align/size the circle relative to the view's bounds. Defaults to
97      * [Gravity.NO_GRAVITY].
98      *
99      * Note: using [.setCenterX], [.setCenterY], or
100      * [.setRadius] will automatically clear any conflicting gravity bits.
101      *
102      * @param gravity the [Gravity] flags to use
103      * @return this object, allowing calls to methods in this class to be chained
104      * @see R.styleable.CircleView_android_gravity
105      */
setGravitynull106     fun setGravity(gravity: Int): CircleView {
107         if (this.gravity != gravity) {
108             this.gravity = gravity
109 
110             if (gravity != Gravity.NO_GRAVITY && isLayoutDirectionResolved) {
111                 applyGravity(gravity, layoutDirection)
112             }
113         }
114         return this
115     }
116 
117     /**
118      * @return the ARGB color used to fill the circle
119      */
120     val fillColor: Int
121         get() = mCirclePaint.color
122 
123     /**
124      * Sets the ARGB color used to fill the circle and invalidates only the affected area.
125      *
126      * @param color the ARGB color to use
127      * @return this object, allowing calls to methods in this class to be chained
128      * @see R.styleable.CircleView_fillColor
129      */
setFillColornull130     fun setFillColor(color: Int): CircleView {
131         if (mCirclePaint.color != color) {
132             mCirclePaint.color = color
133 
134             // invalidate the current area
135             invalidate()
136         }
137         return this
138     }
139 
140     /**
141      * Sets the x-coordinate for the center of the circle and invalidates only the affected area.
142      *
143      * @param centerX the x-coordinate to use, relative to the view's bounds
144      * @return this object, allowing calls to methods in this class to be chained
145      * @see R.styleable.CircleView_centerX
146      */
setCenterXnull147     fun setCenterX(centerX: Float): CircleView {
148         val oldCenterX = mCenterX
149         if (oldCenterX != centerX) {
150             mCenterX = centerX
151 
152             // invalidate the old/new areas
153             invalidate()
154         }
155 
156         // clear the horizontal gravity flags
157         gravity = gravity and Gravity.HORIZONTAL_GRAVITY_MASK.inv()
158 
159         return this
160     }
161 
162     /**
163      * Sets the y-coordinate for the center of the circle and invalidates only the affected area.
164      *
165      * @param centerY the y-coordinate to use, relative to the view's bounds
166      * @return this object, allowing calls to methods in this class to be chained
167      * @see R.styleable.CircleView_centerY
168      */
setCenterYnull169     fun setCenterY(centerY: Float): CircleView {
170         val oldCenterY = mCenterY
171         if (oldCenterY != centerY) {
172             mCenterY = centerY
173 
174             // invalidate the old/new areas
175             invalidate()
176         }
177 
178         // clear the vertical gravity flags
179         gravity = gravity and Gravity.VERTICAL_GRAVITY_MASK.inv()
180 
181         return this
182     }
183 
184     /**
185      * Sets the radius of the circle and invalidates only the affected area.
186      *
187      * @param radius the radius to use
188      * @return this object, allowing calls to methods in this class to be chained
189      * @see R.styleable.CircleView_radius
190      */
setRadiusnull191     fun setRadius(radius: Float): CircleView {
192         val oldRadius = this.radius
193         if (oldRadius != radius) {
194             this.radius = radius
195 
196             // invalidate the old/new areas
197             invalidate()
198         }
199 
200         // clear the fill gravity flags
201         if (gravity and Gravity.FILL_HORIZONTAL == Gravity.FILL_HORIZONTAL) {
202             gravity = gravity and Gravity.FILL_HORIZONTAL.inv()
203         }
204         if (gravity and Gravity.FILL_VERTICAL == Gravity.FILL_VERTICAL) {
205             gravity = gravity and Gravity.FILL_VERTICAL.inv()
206         }
207 
208         return this
209     }
210 
211     /**
212      * Applies the specified `gravity` and `layoutDirection`, adjusting the alignment
213      * and size of the circle depending on the resolved [Gravity] flags. Also invalidates the
214      * affected area if necessary.
215      *
216      * @param gravity the [Gravity] the [Gravity] flags to use
217      * @param layoutDirection the layout direction used to resolve the absolute gravity
218      */
219     @SuppressLint("RtlHardcoded")
applyGravitynull220     private fun applyGravity(gravity: Int, layoutDirection: Int) {
221         val absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection)
222 
223         val oldRadius = radius
224         val oldCenterX = mCenterX
225         val oldCenterY = mCenterY
226 
227         when (absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
228             Gravity.LEFT -> mCenterX = 0.0f
229             Gravity.CENTER_HORIZONTAL, Gravity.FILL_HORIZONTAL -> mCenterX = width / 2.0f
230             Gravity.RIGHT -> mCenterX = width.toFloat()
231         }
232 
233         when (absoluteGravity and Gravity.VERTICAL_GRAVITY_MASK) {
234             Gravity.TOP -> mCenterY = 0.0f
235             Gravity.CENTER_VERTICAL, Gravity.FILL_VERTICAL -> mCenterY = height / 2.0f
236             Gravity.BOTTOM -> mCenterY = height.toFloat()
237         }
238 
239         when (absoluteGravity and Gravity.FILL) {
240             Gravity.FILL -> radius = min(width, height) / 2.0f
241             Gravity.FILL_HORIZONTAL -> radius = width / 2.0f
242             Gravity.FILL_VERTICAL -> radius = height / 2.0f
243         }
244 
245         if (oldCenterX != mCenterX || oldCenterY != mCenterY || oldRadius != radius) {
246             invalidate()
247         }
248     }
249 
250     companion object {
251         /**
252          * A Property wrapper around the fillColor functionality handled by the
253          * [.setFillColor] and [.getFillColor] methods.
254          */
255         @JvmField
256         val FILL_COLOR: Property<CircleView, Int> =
257                 object : Property<CircleView, Int>(Int::class.java, "fillColor") {
getnull258                     override fun get(view: CircleView): Int {
259                         return view.fillColor
260                     }
261 
setnull262                     override fun set(view: CircleView, value: Int) {
263                         view.setFillColor(value)
264                     }
265                 }
266 
267         /**
268          * A Property wrapper around the radius functionality handled by the
269          * [.setRadius] and [.getRadius] methods.
270          */
271         @JvmField
272         val RADIUS: Property<CircleView, Float> =
273                 object : Property<CircleView, Float>(Float::class.java, "radius") {
getnull274                     override fun get(view: CircleView): Float {
275                         return view.radius
276                     }
277 
setnull278                     override fun set(view: CircleView, value: Float) {
279                         view.setRadius(value)
280                     }
281                 }
282     }
283 }