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 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.view.View
22 import android.widget.LinearLayout
23 import com.android.wallpaper.R
24 import com.android.wallpaper.model.wallpaper.DeviceDisplayType
25 import com.android.wallpaper.picker.preview.ui.view.DualDisplayAspectRatioLayout.Companion.getViewId
26 
27 /**
28  * This LinearLayout view group implements the dual preview view for the small preview screen for
29  * foldable devices.
30  */
31 class DualDisplayAspectRatioLayout2(context: Context, attrs: AttributeSet?) :
32     LinearLayout(context, attrs) {
33 
34     private var previewDisplaySizes: Map<DeviceDisplayType, Point>? = null
35     private var firstMeasuredWidth = 0
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         if (firstMeasuredWidth == 0) {
50             firstMeasuredWidth = measuredWidth
51         }
52 
53         // there are three spaces to consider
54         // the margin before the folded preview, the margin in between the folded and unfolded and
55         // the margin after the unfolded view
56         val spaceBetweenPreviews =
57             resources.getDimension(R.dimen.foldable_small_preview_space_between_preview)
58 
59         val ratio = 1.0 - SCREEN_WIDTH_RATIO_FOR_NEXT_PAGE
60         val singlePageWidth = (firstMeasuredWidth * ratio).toFloat()
61         val parentWidth = singlePageWidth - spaceBetweenPreviews
62 
63         val smallDisplaySize = checkNotNull(getPreviewDisplaySize(DeviceDisplayType.FOLDED))
64         val largeDisplaySize = checkNotNull(getPreviewDisplaySize(DeviceDisplayType.UNFOLDED))
65 
66         // calculate the aspect ratio (ar) of the folded display
67         val smallDisplayAR = smallDisplaySize.x.toFloat() / smallDisplaySize.y
68 
69         // calculate the aspect ratio of the unfolded display
70         val largeDisplayAR = largeDisplaySize.x.toFloat() / largeDisplaySize.y
71 
72         // Width based calculation
73         var newHeight = parentWidth / (largeDisplayAR + smallDisplayAR)
74         if (newHeight > measuredHeight) {
75             // If new height derived from width is larger than original height, use height based
76             // calculation.
77             newHeight = measuredHeight.toFloat()
78         }
79 
80         val widthFolded = newHeight * smallDisplayAR
81         val widthUnfolded = newHeight * largeDisplayAR
82 
83         val foldedView = findViewById<View>(DeviceDisplayType.FOLDED.getViewId())
84         foldedView?.measure(
85             MeasureSpec.makeMeasureSpec(widthFolded.toInt(), MeasureSpec.EXACTLY),
86             MeasureSpec.makeMeasureSpec(newHeight.toInt(), MeasureSpec.EXACTLY),
87         )
88 
89         val unfoldedView = findViewById<View>(DeviceDisplayType.UNFOLDED.getViewId())
90         unfoldedView?.measure(
91             MeasureSpec.makeMeasureSpec(widthUnfolded.toInt(), MeasureSpec.EXACTLY),
92             MeasureSpec.makeMeasureSpec(newHeight.toInt(), MeasureSpec.EXACTLY),
93         )
94 
95         val marginPixels =
96             context.resources.getDimension(R.dimen.small_preview_inter_preview_margin).toInt()
97 
98         setMeasuredDimension(
99             MeasureSpec.makeMeasureSpec(
100                 (widthFolded + widthUnfolded + 2 * marginPixels).toInt(),
101                 MeasureSpec.EXACTLY,
102             ),
103             MeasureSpec.makeMeasureSpec(newHeight.toInt(), MeasureSpec.EXACTLY),
104         )
105     }
106 
onLayoutnull107     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
108         // margins
109         val spaceBetweenPreviews =
110             resources.getDimension(R.dimen.foldable_small_preview_space_between_preview).toInt()
111 
112         // the handheld preview will be position first
113         val foldedView = getChildAt(0)
114         val foldedViewWidth = foldedView.measuredWidth
115         val foldedViewHeight = foldedView.measuredHeight
116         foldedView.layout(0, 0, foldedViewWidth, foldedViewHeight)
117 
118         // the unfolded view will be position after
119         val unfoldedView = getChildAt(1)
120         val unfoldedViewWidth = unfoldedView.measuredWidth
121         val unfoldedViewHeight = unfoldedView.measuredHeight
122         unfoldedView.layout(
123             foldedViewWidth + spaceBetweenPreviews,
124             0,
125             unfoldedViewWidth + foldedViewWidth + spaceBetweenPreviews,
126             unfoldedViewHeight,
127         )
128     }
129 
setDisplaySizesnull130     fun setDisplaySizes(displaySizes: Map<DeviceDisplayType, Point>) {
131         previewDisplaySizes = displaySizes
132     }
133 
getPreviewDisplaySizenull134     fun getPreviewDisplaySize(display: DeviceDisplayType): Point? {
135         return previewDisplaySizes?.get(display)
136     }
137 
138     companion object {
139         /** Defines percentage of the screen width is used for showing part of the next page. */
140         private const val SCREEN_WIDTH_RATIO_FOR_NEXT_PAGE = 0.1
141 
142         /** Defines children view ids for [DualDisplayAspectRatioLayout2]. */
DeviceDisplayTypenull143         fun DeviceDisplayType.getViewId(): Int {
144             return when (this) {
145                 DeviceDisplayType.SINGLE ->
146                     throw IllegalStateException(
147                         "DualDisplayAspectRatioLayout does not supper handheld DeviceDisplayType"
148                     )
149                 DeviceDisplayType.FOLDED -> R.id.small_preview_folded_preview
150                 DeviceDisplayType.UNFOLDED -> R.id.small_preview_unfolded_preview
151             }
152         }
153     }
154 }
155