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.customization.R 25 import com.android.systemui.plugins.clocks.AlarmData 26 import com.android.systemui.plugins.clocks.ClockAnimations 27 import com.android.systemui.plugins.clocks.ClockEvents 28 import com.android.systemui.plugins.clocks.ClockFaceConfig 29 import com.android.systemui.plugins.clocks.ClockFaceController 30 import com.android.systemui.plugins.clocks.ClockFaceEvents 31 import com.android.systemui.plugins.clocks.ClockFaceLayout 32 import com.android.systemui.plugins.clocks.ClockFontAxisSetting 33 import com.android.systemui.plugins.clocks.DefaultClockFaceLayout 34 import com.android.systemui.plugins.clocks.ThemeConfig 35 import com.android.systemui.plugins.clocks.WeatherData 36 import com.android.systemui.plugins.clocks.ZenData 37 import com.android.systemui.shared.clocks.view.FlexClockView 38 import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView 39 import java.util.Locale 40 import java.util.TimeZone 41 import kotlin.math.max 42 43 // TODO(b/364680879): Merge w/ ComposedDigitalLayerController 44 class FlexClockFaceController( 45 clockCtx: ClockContext, 46 face: ClockFace, 47 private val isLargeClock: Boolean, 48 ) : ClockFaceController { 49 override val view: View 50 get() = layerController.view 51 52 override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true) 53 54 override var theme = ThemeConfig(true, clockCtx.settings.seedColor) 55 56 private val keyguardLargeClockTopMargin = 57 clockCtx.resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin) 58 val layerController: SimpleClockLayerController 59 val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm") 60 61 init { 62 val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) 63 lp.gravity = Gravity.CENTER 64 65 val layer = face.layers[0] 66 67 layerController = 68 if (isLargeClock) { 69 ComposedDigitalLayerController(clockCtx, layer as ComposedDigitalHandLayer) 70 } else { 71 val childView = SimpleDigitalClockTextView(clockCtx) 72 SimpleDigitalHandLayerController(clockCtx, layer as DigitalHandLayer, childView) 73 } 74 layerController.view.layoutParams = lp 75 } 76 77 /** See documentation at [FlexClockView.offsetGlyphsForStepClockAnimation]. */ offsetGlyphsForStepClockAnimationnull78 private fun offsetGlyphsForStepClockAnimation( 79 clockStartLeft: Int, 80 direction: Int, 81 fraction: Float, 82 ) { 83 (view as? FlexClockView)?.offsetGlyphsForStepClockAnimation( 84 clockStartLeft, 85 direction, 86 fraction, 87 ) 88 } 89 90 override val layout: ClockFaceLayout = <lambda>null91 DefaultClockFaceLayout(view).apply { 92 views[0].id = 93 if (isLargeClock) R.id.lockscreen_clock_view_large else R.id.lockscreen_clock_view 94 } 95 96 override val events = FlexClockFaceEvents() 97 98 // TODO(b/364680879): Remove ClockEvents 99 inner class FlexClockFaceEvents : ClockEvents, ClockFaceEvents { 100 override var isReactiveTouchInteractionEnabled = false 101 get() = field 102 set(value) { 103 field = value 104 layerController.events.isReactiveTouchInteractionEnabled = value 105 } 106 onTimeTicknull107 override fun onTimeTick() { 108 timespecHandler.updateTime() 109 view.contentDescription = timespecHandler.getContentDescription() 110 layerController.faceEvents.onTimeTick() 111 } 112 onTimeZoneChangednull113 override fun onTimeZoneChanged(timeZone: TimeZone) { 114 timespecHandler.timeZone = timeZone 115 layerController.events.onTimeZoneChanged(timeZone) 116 } 117 onTimeFormatChangednull118 override fun onTimeFormatChanged(is24Hr: Boolean) { 119 timespecHandler.is24Hr = is24Hr 120 layerController.events.onTimeFormatChanged(is24Hr) 121 } 122 onLocaleChangednull123 override fun onLocaleChanged(locale: Locale) { 124 timespecHandler.updateLocale(locale) 125 layerController.events.onLocaleChanged(locale) 126 } 127 onFontSettingChangednull128 override fun onFontSettingChanged(fontSizePx: Float) { 129 layerController.faceEvents.onFontSettingChanged(fontSizePx) 130 view.requestLayout() 131 } 132 onThemeChangednull133 override fun onThemeChanged(theme: ThemeConfig) { 134 this@FlexClockFaceController.theme = theme 135 layerController.faceEvents.onThemeChanged(theme) 136 } 137 onFontAxesChangednull138 override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) { 139 layerController.events.onFontAxesChanged(axes) 140 } 141 142 /** 143 * targetRegion passed to all customized clock applies counter translationY of 144 * KeyguardStatusView and keyguard_large_clock_top_margin from default clock 145 */ onTargetRegionChangednull146 override fun onTargetRegionChanged(targetRegion: Rect?) { 147 // When a clock needs to be aligned with screen, like weather clock 148 // it needs to offset back the translation of keyguard_large_clock_top_margin 149 if (isLargeClock && (view as FlexClockView).isAlignedWithScreen()) { 150 val topMargin = keyguardLargeClockTopMargin 151 targetRegion?.let { 152 val (_, yDiff) = computeLayoutDiff(view, it, isLargeClock) 153 // In LS, we use yDiff to counter translate 154 // the translation of KeyguardLargeClockTopMargin 155 // With the targetRegion passed from picker, 156 // we will have yDiff = 0, no translation is needed for weather clock 157 if (yDiff.toInt() != 0) view.translationY = yDiff - topMargin / 2 158 } 159 return 160 } 161 162 var maxWidth = 0f 163 var maxHeight = 0f 164 165 layerController.faceEvents.onTargetRegionChanged(targetRegion) 166 maxWidth = max(maxWidth, view.layoutParams.width.toFloat()) 167 maxHeight = max(maxHeight, view.layoutParams.height.toFloat()) 168 169 val lp = 170 if (maxHeight <= 0 || maxWidth <= 0 || targetRegion == null) { 171 // No specified width/height. Just match parent size. 172 FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) 173 } else { 174 // Scale to fit in targetRegion based on largest child elements. 175 val ratio = maxWidth / maxHeight 176 val targetRatio = targetRegion.width() / targetRegion.height().toFloat() 177 val scale = 178 if (ratio > targetRatio) targetRegion.width() / maxWidth 179 else targetRegion.height() / maxHeight 180 181 FrameLayout.LayoutParams( 182 (maxWidth * scale).toInt(), 183 (maxHeight * scale).toInt(), 184 ) 185 } 186 187 lp.gravity = Gravity.CENTER 188 view.layoutParams = lp 189 targetRegion?.let { 190 val (xDiff, yDiff) = computeLayoutDiff(view, it, isLargeClock) 191 view.translationX = xDiff 192 view.translationY = yDiff 193 } 194 } 195 onSecondaryDisplayChangednull196 override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} 197 onWeatherDataChangednull198 override fun onWeatherDataChanged(data: WeatherData) { 199 layerController.events.onWeatherDataChanged(data) 200 } 201 onAlarmDataChangednull202 override fun onAlarmDataChanged(data: AlarmData) { 203 layerController.events.onAlarmDataChanged(data) 204 } 205 onZenDataChangednull206 override fun onZenDataChanged(data: ZenData) { 207 layerController.events.onZenDataChanged(data) 208 } 209 } 210 211 override val animations = 212 object : ClockAnimations { enternull213 override fun enter() { 214 layerController.animations.enter() 215 } 216 dozenull217 override fun doze(fraction: Float) { 218 layerController.animations.doze(fraction) 219 } 220 foldnull221 override fun fold(fraction: Float) { 222 layerController.animations.fold(fraction) 223 } 224 chargenull225 override fun charge() { 226 layerController.animations.charge() 227 } 228 onPickerCarouselSwipingnull229 override fun onPickerCarouselSwiping(swipingFraction: Float) { 230 face.pickerScale?.let { 231 view.scaleX = swipingFraction * (1 - it.scaleX) + it.scaleX 232 view.scaleY = swipingFraction * (1 - it.scaleY) + it.scaleY 233 } 234 if (isLargeClock && !(view as FlexClockView).isAlignedWithScreen()) { 235 view.translationY = keyguardLargeClockTopMargin / 2F * swipingFraction 236 } 237 layerController.animations.onPickerCarouselSwiping(swipingFraction) 238 view.invalidate() 239 } 240 onPositionUpdatednull241 override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) { 242 layerController.animations.onPositionUpdated(fromLeft, direction, fraction) 243 if (isLargeClock) offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction) 244 } 245 onPositionUpdatednull246 override fun onPositionUpdated(distance: Float, fraction: Float) { 247 layerController.animations.onPositionUpdated(distance, fraction) 248 // TODO(b/378128811) port stepping animation 249 } 250 } 251 } 252