/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.window.embedding
import android.annotation.SuppressLint
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
import androidx.window.core.SpecificationComputer.Companion.startSpecification
import androidx.window.core.VerificationMode
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
/**
* Attributes that describe how the parent window (typically the activity task
* window) is split between the primary and secondary activity containers,
* including:
* - Split type — Categorizes the split and specifies the sizes of the
* primary and secondary activity containers relative to the parent bounds
* - Layout direction — Specifies whether the parent window is split
* vertically or horizontally and in which direction the primary and
* secondary containers are respectively positioned (left to right, right to
* left, top to bottom, and so forth)
* - Animation background color — The color of the background during
* animation of the split involving this `SplitAttributes` object if the
* animation requires a background
*
* Attributes can be configured by:
* - Setting the default `SplitAttributes` using
* [SplitPairRule.Builder.setDefaultSplitAttributes] or
* [SplitPlaceholderRule.Builder.setDefaultSplitAttributes].
* - Setting `splitRatio`, `splitLayoutDirection`, and
* `animationBackgroundColor` attributes in `<SplitPairRule>` or
* `<SplitPlaceholderRule>` tags in an XML configuration file. The
* attributes are parsed as [SplitType], [LayoutDirection], and
* [BackgroundColor], respectively. Note that [SplitType.HingeSplitType]
* is not supported XML format.
* - Using
* [SplitAttributesCalculator.computeSplitAttributesForParams] to customize
* the `SplitAttributes` for a given device and window state.
*
* @see SplitAttributes.SplitType
* @see SplitAttributes.LayoutDirection
*/
class SplitAttributes @RestrictTo(LIBRARY_GROUP) constructor(
/**
* The split type attribute. Defaults to an equal split of the parent window
* for the primary and secondary containers.
*/
val splitType: SplitType = SPLIT_TYPE_EQUAL,
/**
* The layout direction attribute for the parent window split. The default
* is based on locale.
*/
val layoutDirection: LayoutDirection = LOCALE,
) {
/**
* The type of parent window split, which defines the proportion of the
* parent window occupied by the primary and secondary activity containers.
*/
class SplitType internal constructor(
/**
* The description of this `SplitType`.
*/
internal val description: String,
/**
* An identifier for the split type.
*
* Used in the evaluation in the `equals()` method.
*/
internal val value: Float,
) {
/**
* A string representation of this split type.
*
* @return The string representation of the object.
*/
override fun toString(): String = description
/**
* Determines whether this object is the same type of split as the
* compared object.
*
* @param other The object to compare to this object.
* @return True if the objects are the same split type, false otherwise.
*/
override fun equals(other: Any?): Boolean {
if (other === this) return true
if (other !is SplitType) return false
return value == other.value &&
description == other.description
}
/**
* Returns a hash code for this split type.
*
* @return The hash code for this object.
*/
override fun hashCode(): Int = description.hashCode() + 31 * value.hashCode()
/**
* Methods that create various split types.
*/
companion object {
/**
* Creates a split type based on the proportion of the parent window
* occupied by the primary container of the split.
*
* Values in the non-inclusive range (0.0, 1.0) define the size of
* the primary container relative to the size of the parent window:
* - 0.5 — Primary container occupies half of the parent
* window; secondary container, the other half
* - > 0.5 — Primary container occupies a larger proportion
* of the parent window than the secondary container
* - < 0.5 — Primary container occupies a smaller
* proportion of the parent window than the secondary container
*
* @param ratio The proportion of the parent window occupied by the
* primary container of the split.
* @return An instance of `SplitType` with the specified ratio.
*/
@JvmStatic
fun ratio(
@FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
ratio: Float
): SplitType {
val checkedRatio = ratio.startSpecification(
TAG,
VerificationMode.STRICT
).require("Ratio must be in range (0.0, 1.0). " +
"Use SplitType.expandContainers() instead of 0 or 1.") {
ratio in 0.0..1.0 && ratio !in arrayOf(0.0f, 1.0f)
}.compute()!!
return SplitType("ratio:$checkedRatio", checkedRatio)
}
/**
* A split type in which the primary and secondary activity containers each expand to
* fill the parent window; the secondary container overlays the primary container.
*
* It is useful to use this `SplitType` with the function set in
* [SplitController.setSplitAttributesCalculator] to expand the activity containers in
* some device or window states. The following sample shows how to always fill the
* parent bounds if the device is in portrait orientation:
*
* @sample androidx.window.samples.embedding.expandContainersInPortrait
*/
@JvmField
val SPLIT_TYPE_EXPAND = SplitType("expandContainers", 0.0f)
/**
* A split type in which the primary and secondary containers occupy equal portions of
* the parent window.
*
* Serves as the default [SplitType].
*/
@JvmField
val SPLIT_TYPE_EQUAL = ratio(0.5f)
/**
* A split type in which the split ratio conforms to the
* position of a hinge or separating fold in the device display.
*
* The split type works only if:
* <ul>
* <li>The host task is not in multi-window mode (e.g.,
* split-screen mode or picture-in-picture mode)</li>
* <li>The device has a hinge or separating fold reported by
* [androidx.window.layout.FoldingFeature.isSeparating]</li>
* <li>The hinge or separating fold orientation matches how the
* parent bounds are split:
* <ul style="list-style-type: circle;">
* <li>The hinge or fold orientation is vertical, and
* the parent bounds are also split vertically
* (containers are side by side)</li>
* <li>The hinge or fold orientation is horizontal, and
* the parent bounds are also split horizontally
* (containers are top and bottom)</li>
* </ul>
* </li>
* </ul>
*
* Otherwise, this `SplitType` fallback to show the split with [SPLIT_TYPE_EQUAL].
*
* If the app wants to have another fallback `SplitType` if [SPLIT_TYPE_HINGE] cannot
* be applied. It is suggested to use [SplitController.setSplitAttributesCalculator] to
* customize the fallback `SplitType`.
*
* The following sample shows how to fallback to [SPLIT_TYPE_EXPAND]
* if there's no hinge area in the parent window container bounds.
*
* @sample androidx.window.samples.embedding.fallbackToExpandContainersForSplitTypeHinge
*/
@JvmField
val SPLIT_TYPE_HINGE = SplitType("hinge", -1.0f)
// TODO(b/241044092): add XML support to SPLIT_TYPE_HINGE
/**
* Returns a `SplitType` with the given `value`.
*/
@SuppressLint("Range") // value = 0.0 is covered.
internal fun buildSplitTypeFromValue(
@FloatRange(from = 0.0, to = 1.0, toInclusive = false) value: Float
) = if (value == SPLIT_TYPE_EXPAND.value) {
SPLIT_TYPE_EXPAND
} else {
ratio(value)
}
}
}
/**
* The layout direction of the primary and secondary activity containers.
*/
class LayoutDirection private constructor(
/**
* The description of this `LayoutDirection`.
*/
private val description: String,
/**
* The enum value defined in `splitLayoutDirection` attributes in
* `attrs.xml`.
*/
internal val value: Int,
) {
/**
* A string representation of this `LayoutDirection`.
*
* @return The string representation of the object.
*/
override fun toString(): String = description
/**
* Non-public properties and methods.
*/
companion object {
/**
* Specifies that the parent bounds are split vertically (side to
* side).
*
* The direction of the primary and secondary containers is deduced
* from the locale as either `LEFT_TO_RIGHT` or `RIGHT_TO_LEFT`.
*
* See also [layoutDirection].
*/
@JvmField
val LOCALE = LayoutDirection("LOCALE", 0)
/**
* Specifies that the parent bounds are split vertically (side to
* side).
*
* Places the primary container in the left portion of the parent
* window, and the secondary container in the right portion.
*
* <img width="70%" height="70%" src="/images/guide/topics/large-screens/activity-embedding/reference-docs/a_to_a_b_ltr.png" alt="Activity A starts activity B to the right."/>
*
* See also [layoutDirection].
*/
@JvmField
val LEFT_TO_RIGHT = LayoutDirection("LEFT_TO_RIGHT", 1)
/**
* Specifies that the parent bounds are split vertically (side to
* side).
*
* Places the primary container in the right portion of the parent
* window, and the secondary container in the left portion.
*
* <img width="70%" height="70%" src="/images/guide/topics/large-screens/activity-embedding/reference-docs/a_to_a_b_rtl.png" alt="Activity A starts activity B to the left."/>
*
* See also [layoutDirection].
*/
@JvmField
val RIGHT_TO_LEFT = LayoutDirection("RIGHT_TO_LEFT", 2)
/**
* Specifies that the parent bounds are split horizontally (top and
* bottom).
*
* Places the primary container in the top portion of the parent
* window, and the secondary container in the bottom portion.
*
* <img width="70%" height="70%" src="/images/guide/topics/large-screens/activity-embedding/reference-docs/a_to_a_b_ttb.png" alt="Activity A starts activity B to the bottom."/>
*
* If the horizontal layout direction is not supported on the
* device, layout direction falls back to `LOCALE`.
*
* See also [layoutDirection].
*/
@JvmField
val TOP_TO_BOTTOM = LayoutDirection("TOP_TO_BOTTOM", 3)
/**
* Specifies that the parent bounds are split horizontally (top and
* bottom).
*
* Places the primary container in the bottom portion of the parent
* window, and the secondary container in the top portion.
*
* <img width="70%" height="70%" src="/images/guide/topics/large-screens/activity-embedding/reference-docs/a_to_a_b_btt.png" alt="Activity A starts activity B to the top."/>
*
* If the horizontal layout direction is not supported on the
* device, layout direction falls back to `LOCALE`.
*
* See also [layoutDirection].
*/
@JvmField
val BOTTOM_TO_TOP = LayoutDirection("BOTTOM_TO_TOP", 4)
/**
* Returns `LayoutDirection` with the given `value`.
*/
@JvmStatic
internal fun getLayoutDirectionFromValue(
@IntRange(from = 0, to = 4) value: Int
) = when (value) {
LEFT_TO_RIGHT.value -> LEFT_TO_RIGHT
RIGHT_TO_LEFT.value -> RIGHT_TO_LEFT
LOCALE.value -> LOCALE
TOP_TO_BOTTOM.value -> TOP_TO_BOTTOM
BOTTOM_TO_TOP.value -> BOTTOM_TO_TOP
else -> throw IllegalArgumentException("Undefined value:$value")
}
}
}
/**
* Non-public properties and methods.
*/
companion object {
private val TAG = SplitAttributes::class.java.simpleName
}
/**
* Returns a hash code for this `SplitAttributes` object.
*
* @return The hash code for this object.
*/
override fun hashCode(): Int {
var result = splitType.hashCode()
result = result * 31 + layoutDirection.hashCode()
return result
}
/**
* Determines whether this object has the same split attributes as the
* compared object.
*
* @param other The object to compare to this object.
* @return True if the objects have the same split attributes, false
* otherwise.
*/
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SplitAttributes) return false
return splitType == other.splitType &&
layoutDirection == other.layoutDirection
}
/**
* A string representation of this `SplitAttributes` object.
*
* @return The string representation of the object.
*/
override fun toString(): String =
"${SplitAttributes::class.java.simpleName}:" +
"{splitType=$splitType, layoutDir=$layoutDirection }"
/**
* Builder for creating an instance of [SplitAttributes].
*
* - The default split type is an equal split between primary and secondary
* containers.
* - The default layout direction is based on locale.
* - The default animation background color is to use the current theme
* window background color.
*/
class Builder {
private var splitType = SPLIT_TYPE_EQUAL
private var layoutDirection = LOCALE
/**
* Sets the split type attribute.
*
* The default is an equal split between primary and secondary
* containers.
*
* @param type The split type attribute.
* @return This `Builder`.
*
* @see SplitAttributes.SplitType
*/
fun setSplitType(type: SplitType): Builder = apply { splitType = type }
/**
* Sets the split layout direction attribute.
*
* The default is based on locale.
*
* @param layoutDirection The layout direction attribute.
* @return This `Builder`.
*
* @see SplitAttributes.LayoutDirection
*/
fun setLayoutDirection(layoutDirection: LayoutDirection): Builder =
apply { this.layoutDirection = layoutDirection }
/**
* Builds a `SplitAttributes` instance with the attributes specified by
* [setSplitType] and [setLayoutDirection].
*
* @return The new `SplitAttributes` instance.
*/
fun build(): SplitAttributes = SplitAttributes(splitType, layoutDirection)
}
}