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 }