• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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(mCenterX, mCenterY, radius)
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(oldCenterX, mCenterY, radius)
154             invalidate(centerX, mCenterY, radius)
155         }
156 
157         // clear the horizontal gravity flags
158         gravity = gravity and Gravity.HORIZONTAL_GRAVITY_MASK.inv()
159 
160         return this
161     }
162 
163     /**
164      * Sets the y-coordinate for the center of the circle and invalidates only the affected area.
165      *
166      * @param centerY the y-coordinate to use, relative to the view's bounds
167      * @return this object, allowing calls to methods in this class to be chained
168      * @see R.styleable.CircleView_centerY
169      */
setCenterYnull170     fun setCenterY(centerY: Float): CircleView {
171         val oldCenterY = mCenterY
172         if (oldCenterY != centerY) {
173             mCenterY = centerY
174 
175             // invalidate the old/new areas
176             invalidate(mCenterX, oldCenterY, radius)
177             invalidate(mCenterX, centerY, radius)
178         }
179 
180         // clear the vertical gravity flags
181         gravity = gravity and Gravity.VERTICAL_GRAVITY_MASK.inv()
182 
183         return this
184     }
185 
186     /**
187      * Sets the radius of the circle and invalidates only the affected area.
188      *
189      * @param radius the radius to use
190      * @return this object, allowing calls to methods in this class to be chained
191      * @see R.styleable.CircleView_radius
192      */
setRadiusnull193     fun setRadius(radius: Float): CircleView {
194         val oldRadius = this.radius
195         if (oldRadius != radius) {
196             this.radius = radius
197 
198             // invalidate the old/new areas
199             invalidate(mCenterX, mCenterY, oldRadius)
200             if (radius > oldRadius) {
201                 invalidate(mCenterX, mCenterY, radius)
202             }
203         }
204 
205         // clear the fill gravity flags
206         if (gravity and Gravity.FILL_HORIZONTAL == Gravity.FILL_HORIZONTAL) {
207             gravity = gravity and Gravity.FILL_HORIZONTAL.inv()
208         }
209         if (gravity and Gravity.FILL_VERTICAL == Gravity.FILL_VERTICAL) {
210             gravity = gravity and Gravity.FILL_VERTICAL.inv()
211         }
212 
213         return this
214     }
215 
216     /**
217      * Invalidates the rectangular area that circumscribes the circle defined by `centerX`,
218      * `centerY`, and `radius`.
219      */
invalidatenull220     private fun invalidate(centerX: Float, centerY: Float, radius: Float) {
221         invalidate((centerX - radius - 0.5f).toInt(), (centerY - radius - 0.5f).toInt(),
222                 (centerX + radius + 0.5f).toInt(), (centerY + radius + 0.5f).toInt())
223     }
224 
225     /**
226      * Applies the specified `gravity` and `layoutDirection`, adjusting the alignment
227      * and size of the circle depending on the resolved [Gravity] flags. Also invalidates the
228      * affected area if necessary.
229      *
230      * @param gravity the [Gravity] the [Gravity] flags to use
231      * @param layoutDirection the layout direction used to resolve the absolute gravity
232      */
233     @SuppressLint("RtlHardcoded")
applyGravitynull234     private fun applyGravity(gravity: Int, layoutDirection: Int) {
235         val absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection)
236 
237         val oldRadius = radius
238         val oldCenterX = mCenterX
239         val oldCenterY = mCenterY
240 
241         when (absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
242             Gravity.LEFT -> mCenterX = 0.0f
243             Gravity.CENTER_HORIZONTAL, Gravity.FILL_HORIZONTAL -> mCenterX = width / 2.0f
244             Gravity.RIGHT -> mCenterX = width.toFloat()
245         }
246 
247         when (absoluteGravity and Gravity.VERTICAL_GRAVITY_MASK) {
248             Gravity.TOP -> mCenterY = 0.0f
249             Gravity.CENTER_VERTICAL, Gravity.FILL_VERTICAL -> mCenterY = height / 2.0f
250             Gravity.BOTTOM -> mCenterY = height.toFloat()
251         }
252 
253         when (absoluteGravity and Gravity.FILL) {
254             Gravity.FILL -> radius = min(width, height) / 2.0f
255             Gravity.FILL_HORIZONTAL -> radius = width / 2.0f
256             Gravity.FILL_VERTICAL -> radius = height / 2.0f
257         }
258 
259         if (oldCenterX != mCenterX || oldCenterY != mCenterY || oldRadius != radius) {
260             invalidate(oldCenterX, oldCenterY, oldRadius)
261             invalidate(mCenterX, mCenterY, radius)
262         }
263     }
264 
265     companion object {
266         /**
267          * A Property wrapper around the fillColor functionality handled by the
268          * [.setFillColor] and [.getFillColor] methods.
269          */
270         @JvmField
271         val FILL_COLOR: Property<CircleView, Int> =
272                 object : Property<CircleView, Int>(Int::class.java, "fillColor") {
getnull273                     override fun get(view: CircleView): Int {
274                         return view.fillColor
275                     }
276 
setnull277                     override fun set(view: CircleView, value: Int) {
278                         view.setFillColor(value)
279                     }
280                 }
281 
282         /**
283          * A Property wrapper around the radius functionality handled by the
284          * [.setRadius] and [.getRadius] methods.
285          */
286         @JvmField
287         val RADIUS: Property<CircleView, Float> =
288                 object : Property<CircleView, Float>(Float::class.java, "radius") {
getnull289                     override fun get(view: CircleView): Float {
290                         return view.radius
291                     }
292 
setnull293                     override fun set(view: CircleView, value: Float) {
294                         view.setRadius(value)
295                     }
296                 }
297     }
298 }