1 /*
2  * Copyright (C) 2023 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.wallpaper.picker.preview.ui.view
17 
18 import android.content.Context
19 import android.graphics.Point
20 import android.util.AttributeSet
21 import android.widget.LinearLayout
22 import com.android.wallpaper.R
23 import com.android.wallpaper.model.wallpaper.DeviceDisplayType
24 import kotlin.math.max
25 
26 /**
27  * This LinearLayout view group implements the dual preview view for the small preview screen for
28  * foldable devices.
29  */
30 class DualDisplayAspectRatioLayout(
31     context: Context,
32     attrs: AttributeSet?,
33 ) : LinearLayout(context, attrs) {
34 
35     private var previewDisplaySizes: Map<DeviceDisplayType, Point>? = null
36 
37     /**
38      * This measures the desired size of the preview views for both of foldable device's displays.
39      * Each preview view respects the aspect ratio of the display it corresponds to while trying to
40      * have the maximum possible height.
41      */
onMeasurenull42     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
43         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
44 
45         if (previewDisplaySizes == null) {
46             setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
47             return
48         }
49 
50         // there are three spaces to consider
51         // the margin before the folded preview, the margin in between the folded and unfolded and
52         // the margin after the unfolded view
53         val totalMarginPixels =
54             context.resources.getDimension(R.dimen.small_preview_inter_preview_margin).toInt() * 3
55 
56         // TODO: This only works for portrait mode currently, need to incorporate landscape
57         val parentWidth = this.measuredWidth - totalMarginPixels
58 
59         val smallDisplaySize = checkNotNull(getPreviewDisplaySize(DeviceDisplayType.FOLDED))
60         val largeDisplaySize = checkNotNull(getPreviewDisplaySize(DeviceDisplayType.UNFOLDED))
61 
62         // calculate the aspect ratio (ar) of the folded display
63         val smallDisplayAR = smallDisplaySize.x.toFloat() / smallDisplaySize.y
64 
65         // calculate the aspect ratio of the unfolded display
66         val largeDisplayAR = largeDisplaySize.x.toFloat() / largeDisplaySize.y
67 
68         val sizeMultiplier = parentWidth / (largeDisplayAR + smallDisplayAR)
69         val widthFolded = (sizeMultiplier * smallDisplayAR).toInt()
70         val heightFolded = (widthFolded / smallDisplayAR).toInt()
71 
72         val widthUnfolded = (sizeMultiplier * largeDisplayAR).toInt()
73         val heightUnfolded = (widthUnfolded / largeDisplayAR).toInt()
74 
75         val foldedView = getChildAt(0)
76         foldedView.measure(
77             MeasureSpec.makeMeasureSpec(
78                 widthFolded,
79                 MeasureSpec.EXACTLY,
80             ),
81             MeasureSpec.makeMeasureSpec(
82                 heightFolded,
83                 MeasureSpec.EXACTLY,
84             ),
85         )
86 
87         val unfoldedView = getChildAt(1)
88         unfoldedView.measure(
89             MeasureSpec.makeMeasureSpec(
90                 widthUnfolded,
91                 MeasureSpec.EXACTLY,
92             ),
93             MeasureSpec.makeMeasureSpec(
94                 heightUnfolded,
95                 MeasureSpec.EXACTLY,
96             ),
97         )
98 
99         val marginPixels =
100             context.resources.getDimension(R.dimen.small_preview_inter_preview_margin).toInt()
101 
102         setMeasuredDimension(
103             MeasureSpec.makeMeasureSpec(
104                 widthFolded + widthUnfolded + 2 * marginPixels,
105                 MeasureSpec.EXACTLY,
106             ),
107             MeasureSpec.makeMeasureSpec(
108                 max(heightFolded, heightUnfolded),
109                 MeasureSpec.EXACTLY,
110             )
111         )
112     }
113 
onLayoutnull114     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
115         // margins
116         val marginPixels =
117             context.resources.getDimension(R.dimen.small_preview_inter_preview_margin).toInt()
118 
119         // the handheld preview will be position first
120         val foldedView = getChildAt(0)
121         val foldedViewWidth = foldedView.measuredWidth
122         val foldedViewHeight = foldedView.measuredHeight
123         foldedView.layout(0 + marginPixels, 0, foldedViewWidth + marginPixels, foldedViewHeight)
124 
125         // the unfolded view will be position after
126         val unfoldedView = getChildAt(1)
127         val unfoldedViewWidth = unfoldedView.measuredWidth
128         val unfoldedViewHeight = unfoldedView.measuredHeight
129         unfoldedView.layout(
130             foldedViewWidth + 2 * marginPixels,
131             0,
132             unfoldedViewWidth + foldedViewWidth + 2 * marginPixels,
133             unfoldedViewHeight
134         )
135     }
136 
setDisplaySizesnull137     fun setDisplaySizes(displaySizes: Map<DeviceDisplayType, Point>) {
138         previewDisplaySizes = displaySizes
139     }
140 
getPreviewDisplaySizenull141     fun getPreviewDisplaySize(display: DeviceDisplayType): Point? {
142         return previewDisplaySizes?.get(display)
143     }
144 
145     companion object {
146         /** Defines children view ids for [DualDisplayAspectRatioLayout]. */
DeviceDisplayTypenull147         fun DeviceDisplayType.getViewId(): Int {
148             return when (this) {
149                 DeviceDisplayType.SINGLE ->
150                     throw IllegalStateException(
151                         "DualDisplayAspectRatioLayout does not supper handheld DeviceDisplayType"
152                     )
153                 DeviceDisplayType.FOLDED -> R.id.small_preview_folded_preview
154                 DeviceDisplayType.UNFOLDED -> R.id.small_preview_unfolded_preview
155             }
156         }
157     }
158 }
159