/*
* 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 android.graphics.Color
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
import androidx.window.core.SpecificationComputer.Companion.startSpecification
import androidx.window.core.VerificationMode
import androidx.window.embedding.SplitAttributes.BackgroundColor
import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
import androidx.window.embedding.SplitAttributes.SplitType.Companion.splitEqually
/**
* 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
* @see SplitAttributes.BackgroundColor
*/
class SplitAttributes internal constructor(
/**
* The split type attribute. Defaults to an equal split of the parent window
* for the primary and secondary containers.
*/
val splitType: SplitType = splitEqually(),
/**
* The layout direction attribute for the parent window split. The default
* is based on locale.
*/
val layoutDirection: LayoutDirection = LOCALE,
/**
* The color to use for the background color during the animation of
* the split involving this `SplitAttributes` object if the animation
* requires a background.
*
* The default is to use the current theme window background color.
*
* @see BackgroundColor.color
* @see BackgroundColor.DEFAULT
*/
val animationBackgroundColor: BackgroundColor = BackgroundColor.DEFAULT
) {
/**
* The type of parent window split, which defines the proportion of the
* parent window occupied by the primary and secondary activity containers.
*/
open 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()
/**
* A window split that's based on the ratio of the size of the primary
* container to the size of the parent window.
*
* @see SplitAttributes.SplitType.ratio
*/
class RatioSplitType internal constructor(
/**
* The proportion of the parent window occupied by the primary
* container of the split.
*/
@FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
val ratio: Float
) : SplitType("ratio:$ratio", ratio)
/**
* A window split in which the primary and secondary activity containers
* each occupy the entire parent window.
*
* The secondary container overlays the primary container.
*
* @see SplitAttributes.SplitType.ExpandContainersSplitType
*/
class ExpandContainersSplitType internal constructor() : SplitType("expandContainer", 0.0f)
/**
* A parent window split that conforms to a hinge or separating fold in
* the device display.
*
* @see SplitAttributes.SplitType.splitByHinge
*/
class HingeSplitType internal constructor(
/**
* The split type to use if a split based on the device hinge or
* separating fold cannot be determined.
*/
val fallbackSplitType: SplitType
) : SplitType("hinge, fallback=$fallbackSplitType", -1.0f)
/**
* 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 [RatioSplitType] with the specified ratio.
*/
@JvmStatic
fun ratio(
@FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
ratio: Float
): RatioSplitType {
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 RatioSplitType(checkedRatio)
}
private val EXPAND_CONTAINERS = ExpandContainersSplitType()
/**
* Creates 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.
*
* Use this method with the function set in
* [SplitController.setSplitAttributesCalculator] to expand the activity containers in
* some device 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
*
* @return An instance of [ExpandContainersSplitType].
*/
@JvmStatic
fun expandContainers(): ExpandContainersSplitType = EXPAND_CONTAINERS
/**
* Creates a split type in which the primary and secondary
* containers occupy equal portions of the parent window.
*
* Serves as the default [SplitType].
*
* @return A `RatioSplitType` in which the activity containers
* occupy equal portions of the parent window.
*/
@JvmStatic
fun splitEqually(): RatioSplitType = ratio(0.5f)
/**
* Creates 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 is created 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, the method falls back to `fallbackSplitType`.
*
* @param fallbackSplitType The split type to use if a split based
* on the device hinge or separating fold cannot be determined.
* Can be a [RatioSplitType] or [ExpandContainersSplitType].
* Defaults to [SplitType.splitEqually].
* @return An instance of [HingeSplitType] with a fallback split
* type.
*/
@JvmStatic
fun splitByHinge(
fallbackSplitType: SplitType = splitEqually()
): HingeSplitType {
val checkedType = fallbackSplitType.startSpecification(
TAG,
VerificationMode.STRICT
).require(
"FallbackSplitType must be a RatioSplitType or ExpandContainerSplitType"
) {
fallbackSplitType is RatioSplitType ||
fallbackSplitType is ExpandContainersSplitType
}.compute()!!
return HingeSplitType(checkedType)
}
/**
* Returns a `SplitType` with the given `value`.
*/
@SuppressLint("Range") // value = 0.0 is covered.
@JvmStatic
internal fun buildSplitTypeFromValue(
@FloatRange(from = 0.0, to = 1.0, toInclusive = false) value: Float
) = if (value == EXPAND_CONTAINERS.value) {
expandContainers()
} 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")
}
}
}
/**
* Background color to be used for window transition animations in a split if the animation
* requires a background.
*
* @see SplitAttributes.animationBackgroundColor
*/
class BackgroundColor private constructor(
/**
* The description of this `BackgroundColor`.
*/
private val description: String,
/**
* [ColorInt] to represent the color to use as the background color.
*/
@ColorInt
internal val value: Int,
) {
override fun toString() = "BackgroundColor($description)"
override fun equals(other: Any?): Boolean {
if (other === this) return true
if (other !is BackgroundColor) return false
return value == other.value && description == other.description
}
override fun hashCode() = description.hashCode() + 31 * value.hashCode()
/**
* Methods that create various [BackgroundColor].
*/
companion object {
/**
* Creates a [BackgroundColor] to represent the given [color].
*
* Only opaque color is supported.
*
* @param color [ColorInt] of an opaque color.
* @return the [BackgroundColor] representing the [color].
*
* @see [DEFAULT] for the default value, which means to use the
* current theme window background color.
*/
@JvmStatic
fun color(
@IntRange(from = Color.BLACK.toLong(), to = Color.WHITE.toLong())
@ColorInt
color: Int
):
BackgroundColor {
require(Color.BLACK <= color && color <= Color.WHITE) {
"Background color must be opaque"
}
return BackgroundColor("color:${Integer.toHexString(color)}", color)
}
/**
* The special [BackgroundColor] to represent the default value,
* which means to use the current theme window background color.
*/
@JvmField
val DEFAULT = BackgroundColor("DEFAULT", 0)
/**
* Returns a [BackgroundColor] with the given [value]
*/
internal fun buildFromValue(@ColorInt value: Int): BackgroundColor {
return if (Color.alpha(value) != 255) {
// Treat any non-opaque color as the default.
DEFAULT
} else {
color(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()
result = result * 31 + animationBackgroundColor.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 &&
animationBackgroundColor == other.animationBackgroundColor
}
/**
* 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," +
" animationBackgroundColor=$animationBackgroundColor"
/**
* 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: SplitType = splitEqually()
private var layoutDirection = LOCALE
private var animationBackgroundColor = BackgroundColor.DEFAULT
/**
* 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 }
/**
* Sets the color to use for the background color during animation
* of the split involving this `SplitAttributes` object if the animation
* requires a background. Only opaque color is supported.
*
* The default is [BackgroundColor.DEFAULT], which means to use the
* current theme window background color.
*
* @param color The animation background color.
* @return This `Builder`.
*
* @see BackgroundColor.color
* @see BackgroundColor.DEFAULT
*/
fun setAnimationBackgroundColor(color: BackgroundColor): Builder =
apply {
animationBackgroundColor = color
}
/**
* Builds a `SplitAttributes` instance with the attributes specified by
* [setSplitType], [setLayoutDirection], and
* [setAnimationBackgroundColor].
*
* @return The new `SplitAttributes` instance.
*/
fun build(): SplitAttributes = SplitAttributes(splitType, layoutDirection,
animationBackgroundColor)
}
}