1 /* 2 * Copyright (C) 2024 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.systemui.shared.clocks 18 19 import android.graphics.Rect 20 import android.view.Gravity 21 import android.view.View 22 import android.view.ViewGroup.LayoutParams.MATCH_PARENT 23 import android.widget.FrameLayout 24 import com.android.systemui.animation.GSFAxes 25 import com.android.systemui.customization.R 26 import com.android.systemui.plugins.clocks.AlarmData 27 import com.android.systemui.plugins.clocks.ClockAnimations 28 import com.android.systemui.plugins.clocks.ClockAxisStyle 29 import com.android.systemui.plugins.clocks.ClockEvents 30 import com.android.systemui.plugins.clocks.ClockFaceConfig 31 import com.android.systemui.plugins.clocks.ClockFaceController 32 import com.android.systemui.plugins.clocks.ClockFaceEvents 33 import com.android.systemui.plugins.clocks.ClockFaceLayout 34 import com.android.systemui.plugins.clocks.ClockFontAxis.Companion.merge 35 import com.android.systemui.plugins.clocks.DefaultClockFaceLayout 36 import com.android.systemui.plugins.clocks.ThemeConfig 37 import com.android.systemui.plugins.clocks.WeatherData 38 import com.android.systemui.plugins.clocks.ZenData 39 import com.android.systemui.shared.clocks.FlexClockController.Companion.getDefaultAxes 40 import com.android.systemui.shared.clocks.FontUtils.get 41 import com.android.systemui.shared.clocks.FontUtils.set 42 import com.android.systemui.shared.clocks.ViewUtils.computeLayoutDiff 43 import com.android.systemui.shared.clocks.view.FlexClockView 44 import com.android.systemui.shared.clocks.view.HorizontalAlignment 45 import com.android.systemui.shared.clocks.view.VerticalAlignment 46 import java.util.Locale 47 import java.util.TimeZone 48 import kotlin.math.max 49 import kotlin.math.roundToInt 50 51 // TODO(b/364680879): Merge w/ ComposedDigitalLayerController 52 class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: Boolean) : 53 ClockFaceController { 54 override val view: View 55 get() = layerController.view 56 57 override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true) 58 59 override var theme = ThemeConfig(true, clockCtx.settings.seedColor) 60 61 private val keyguardLargeClockTopMargin = 62 clockCtx.resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin) 63 val layerController: SimpleClockLayerController 64 val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm") 65 66 init { 67 layerController = 68 if (isLargeClock) ComposedDigitalLayerController(clockCtx) 69 else SimpleDigitalHandLayerController(clockCtx, SMALL_LAYER_CONFIG, isLargeClock) 70 71 layerController.view.layoutParams = <lambda>null72 FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT).apply { gravity = Gravity.CENTER } 73 } 74 75 /** See documentation at [FlexClockView.offsetGlyphsForStepClockAnimation]. */ offsetGlyphsForStepClockAnimationnull76 private fun offsetGlyphsForStepClockAnimation( 77 clockStartLeft: Int, 78 direction: Int, 79 fraction: Float, 80 ) { 81 (view as? FlexClockView)?.offsetGlyphsForStepClockAnimation( 82 clockStartLeft, 83 direction, 84 fraction, 85 ) 86 } 87 88 override val layout: ClockFaceLayout = <lambda>null89 DefaultClockFaceLayout(view).apply { 90 views[0].id = 91 if (isLargeClock) R.id.lockscreen_clock_view_large else R.id.lockscreen_clock_view 92 } 93 94 override val events = FlexClockFaceEvents() 95 96 // TODO(b/364680879): Remove ClockEvents 97 inner class FlexClockFaceEvents : ClockEvents, ClockFaceEvents { 98 override var isReactiveTouchInteractionEnabled = false 99 get() = field 100 set(value) { 101 field = value 102 layerController.events.isReactiveTouchInteractionEnabled = value 103 } 104 onTimeTicknull105 override fun onTimeTick() { 106 timespecHandler.updateTime() 107 view.contentDescription = timespecHandler.getContentDescription() 108 layerController.faceEvents.onTimeTick() 109 } 110 onTimeZoneChangednull111 override fun onTimeZoneChanged(timeZone: TimeZone) { 112 timespecHandler.timeZone = timeZone 113 layerController.events.onTimeZoneChanged(timeZone) 114 } 115 onTimeFormatChangednull116 override fun onTimeFormatChanged(is24Hr: Boolean) { 117 timespecHandler.is24Hr = is24Hr 118 layerController.events.onTimeFormatChanged(is24Hr) 119 } 120 onLocaleChangednull121 override fun onLocaleChanged(locale: Locale) { 122 timespecHandler.updateLocale(locale) 123 layerController.events.onLocaleChanged(locale) 124 } 125 onFontSettingChangednull126 override fun onFontSettingChanged(fontSizePx: Float) { 127 layerController.faceEvents.onFontSettingChanged(fontSizePx) 128 view.requestLayout() 129 } 130 onThemeChangednull131 override fun onThemeChanged(theme: ThemeConfig) { 132 this@FlexClockFaceController.theme = theme 133 layerController.faceEvents.onThemeChanged(theme) 134 } 135 136 /** 137 * targetRegion passed to all customized clock applies counter translationY of Keyguard and 138 * keyguard_large_clock_top_margin from default clock 139 */ onTargetRegionChangednull140 override fun onTargetRegionChanged(targetRegion: Rect?) { 141 var maxWidth = 0f 142 var maxHeight = 0f 143 144 layerController.faceEvents.onTargetRegionChanged(targetRegion) 145 maxWidth = max(maxWidth, view.layoutParams.width.toFloat()) 146 maxHeight = max(maxHeight, view.layoutParams.height.toFloat()) 147 148 val lp = 149 if (maxHeight <= 0 || maxWidth <= 0 || targetRegion == null) { 150 // No specified width/height. Just match parent size. 151 FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) 152 } else { 153 // Scale to fit in targetRegion based on largest child elements. 154 val ratio = maxWidth / maxHeight 155 val targetRatio = targetRegion.width() / targetRegion.height().toFloat() 156 val scale = 157 if (ratio > targetRatio) targetRegion.width() / maxWidth 158 else targetRegion.height() / maxHeight 159 160 FrameLayout.LayoutParams( 161 (maxWidth * scale).roundToInt(), 162 (maxHeight * scale).roundToInt(), 163 ) 164 } 165 166 lp.gravity = Gravity.CENTER 167 view.layoutParams = lp 168 targetRegion?.let { 169 val diff = view.computeLayoutDiff(it, isLargeClock) 170 view.translationX = diff.x 171 view.translationY = diff.y 172 } 173 } 174 onSecondaryDisplayChangednull175 override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} 176 onWeatherDataChangednull177 override fun onWeatherDataChanged(data: WeatherData) { 178 layerController.events.onWeatherDataChanged(data) 179 } 180 onAlarmDataChangednull181 override fun onAlarmDataChanged(data: AlarmData) { 182 layerController.events.onAlarmDataChanged(data) 183 } 184 onZenDataChangednull185 override fun onZenDataChanged(data: ZenData) { 186 layerController.events.onZenDataChanged(data) 187 } 188 } 189 190 override val animations = 191 object : ClockAnimations { enternull192 override fun enter() { 193 layerController.animations.enter() 194 } 195 dozenull196 override fun doze(fraction: Float) { 197 layerController.animations.doze(fraction) 198 } 199 foldnull200 override fun fold(fraction: Float) { 201 layerController.animations.fold(fraction) 202 } 203 chargenull204 override fun charge() { 205 layerController.animations.charge() 206 } 207 onPickerCarouselSwipingnull208 override fun onPickerCarouselSwiping(swipingFraction: Float) { 209 if (isLargeClock) { 210 view.translationY = keyguardLargeClockTopMargin / 2F * swipingFraction 211 } 212 layerController.animations.onPickerCarouselSwiping(swipingFraction) 213 view.invalidate() 214 } 215 onPositionUpdatednull216 override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) { 217 layerController.animations.onPositionUpdated(fromLeft, direction, fraction) 218 if (isLargeClock) offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction) 219 } 220 onPositionUpdatednull221 override fun onPositionUpdated(distance: Float, fraction: Float) { 222 layerController.animations.onPositionUpdated(distance, fraction) 223 } 224 onFidgetTapnull225 override fun onFidgetTap(x: Float, y: Float) { 226 layerController.animations.onFidgetTap(x, y) 227 } 228 onFontAxesChangednull229 override fun onFontAxesChanged(style: ClockAxisStyle) { 230 var axes = ClockAxisStyle(getDefaultAxes(clockCtx.settings).merge(style)) 231 if (!isLargeClock && axes[GSFAxes.WIDTH] > SMALL_CLOCK_MAX_WDTH) { 232 axes[GSFAxes.WIDTH] = SMALL_CLOCK_MAX_WDTH 233 } 234 235 layerController.animations.onFontAxesChanged(axes) 236 } 237 } 238 239 companion object { 240 val SMALL_CLOCK_MAX_WDTH = 120f 241 242 val SMALL_LAYER_CONFIG = 243 LayerConfig( 244 timespec = DigitalTimespec.TIME_FULL_FORMAT, 245 style = FontTextStyle(fontSizeScale = 0.98f), 246 aodStyle = FontTextStyle(), 247 alignment = DigitalAlignment(HorizontalAlignment.START, VerticalAlignment.CENTER), 248 dateTimeFormat = "h:mm", 249 ) 250 } 251 } 252