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 
17 package com.android.deskclock.stopwatch
18 
19 import android.content.Context
20 import android.util.AttributeSet
21 import android.view.View
22 import android.view.View.MeasureSpec.AT_MOST
23 import android.view.View.MeasureSpec.EXACTLY
24 import android.view.View.MeasureSpec.UNSPECIFIED
25 import android.view.ViewGroup
26 
27 import com.android.deskclock.R
28 
29 import kotlin.math.max
30 
31 /**
32  * Dynamically apportions size the stopwatch circle depending on the preferred width of the laps
33  * list and the container size. Layouts fall into two different buckets:
34  *
35  * When the width of the laps list is less than half the container width, the laps list and
36  * stopwatch display are each centered within half the container.
37  * <pre>
38  * ---------------------------------------------------------------------------
39  * |                                    |               Lap 5                |
40  * |                                    |               Lap 4                |
41  * |             21:45.67               |               Lap 3                |
42  * |                                    |               Lap 2                |
43  * |                                    |               Lap 1                |
44  * ---------------------------------------------------------------------------
45 </pre> *
46  *
47  * When the width of the laps list is greater than half the container width, the laps list is
48  * granted all of the space it requires and the stopwatch display is centered within the remaining
49  * container width.
50  * <pre>
51  * ---------------------------------------------------------------------------
52  * |               |                          Lap 5                          |
53  * |               |                          Lap 4                          |
54  * |   21:45.67    |                          Lap 3                          |
55  * |               |                          Lap 2                          |
56  * |               |                          Lap 1                          |
57  * ---------------------------------------------------------------------------
58 </pre> *
59  */
60 class StopwatchLandscapeLayout : ViewGroup {
61     private var mLapsListView: View? = null
62     private lateinit var mStopwatchView: View
63 
64     constructor(context: Context?) : super(context) {
65     }
66 
67     constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
68     }
69 
70     constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int)
71             : super(context, attrs, defStyleAttr) {
72     }
73 
74     override fun onFinishInflate() {
75         super.onFinishInflate()
76 
77         mLapsListView = findViewById(R.id.laps_list)
78         mStopwatchView = findViewById(R.id.stopwatch_time_wrapper)
79     }
80 
81     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
82         val height: Int = MeasureSpec.getSize(heightMeasureSpec)
83         val width: Int = MeasureSpec.getSize(widthMeasureSpec)
84         val halfWidth = width / 2
85 
86         val minWidthSpec: Int = MeasureSpec.makeMeasureSpec(width, UNSPECIFIED)
87         val maxHeightSpec: Int = MeasureSpec.makeMeasureSpec(height, AT_MOST)
88 
89         // First determine the width of the laps list.
90         val lapsListWidth: Int
91         val lapsListView = mLapsListView
92         if (lapsListView != null && lapsListView.getVisibility() != GONE) {
93             // Measure the intrinsic size of the laps list.
94             lapsListView.measure(minWidthSpec, maxHeightSpec)
95 
96             // Actual laps list width is the larger of half the container and its intrinsic width.
97             lapsListWidth = max(lapsListView.getMeasuredWidth(), halfWidth)
98             val lapsListWidthSpec: Int = MeasureSpec.makeMeasureSpec(lapsListWidth, EXACTLY)
99             lapsListView.measure(lapsListWidthSpec, maxHeightSpec)
100         } else {
101             lapsListWidth = 0
102         }
103 
104         // Stopwatch timer consumes the remaining width of container not granted to laps list.
105         val stopwatchWidth = width - lapsListWidth
106         val stopwatchWidthSpec: Int = MeasureSpec.makeMeasureSpec(stopwatchWidth, EXACTLY)
107         mStopwatchView.measure(stopwatchWidthSpec, maxHeightSpec)
108 
109         // Record the measured size of this container.
110         setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
111     }
112 
113     override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
114         // Compute the space available for layout.
115         val left: Int = getPaddingLeft()
116         val top: Int = getPaddingTop()
117         val right: Int = getWidth() - getPaddingRight()
118         val bottom: Int = getHeight() - getPaddingBottom()
119         val width = right - left
120         val height = bottom - top
121         val halfHeight = height / 2
122         val isLTR = getLayoutDirection() == LAYOUT_DIRECTION_LTR
123 
124         val lapsListWidth: Int
125         val lapsListView = mLapsListView
126         if (lapsListView != null && lapsListView.getVisibility() != GONE) {
127             // Layout the laps list, centering it vertically.
128             lapsListWidth = lapsListView.getMeasuredWidth()
129             val lapsListHeight: Int = lapsListView.getMeasuredHeight()
130             val lapsListTop = top + halfHeight - lapsListHeight / 2
131             val lapsListBottom = lapsListTop + lapsListHeight
132             val lapsListLeft: Int
133             val lapsListRight: Int
134             if (isLTR) {
135                 lapsListLeft = right - lapsListWidth
136                 lapsListRight = right
137             } else {
138                 lapsListLeft = left
139                 lapsListRight = left + lapsListWidth
140             }
141             lapsListView.layout(lapsListLeft, lapsListTop, lapsListRight, lapsListBottom)
142         } else {
143             lapsListWidth = 0
144         }
145 
146         // Layout the stopwatch, centering it horizontally and vertically.
147         val stopwatchWidth: Int = mStopwatchView.getMeasuredWidth()
148         val stopwatchHeight: Int = mStopwatchView.getMeasuredHeight()
149         val stopwatchTop = top + halfHeight - stopwatchHeight / 2
150         val stopwatchBottom = stopwatchTop + stopwatchHeight
151         val stopwatchLeft: Int
152         val stopwatchRight: Int
153         if (isLTR) {
154             stopwatchLeft = left + (width - lapsListWidth - stopwatchWidth) / 2
155             stopwatchRight = stopwatchLeft + stopwatchWidth
156         } else {
157             stopwatchRight = right - (width - lapsListWidth - stopwatchWidth) / 2
158             stopwatchLeft = stopwatchRight - stopwatchWidth
159         }
160 
161         mStopwatchView.layout(stopwatchLeft, stopwatchTop, stopwatchRight, stopwatchBottom)
162     }
163 }