/*
|
* Copyright (C) 2018 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 android.view;
|
|
import static android.view.ViewRootImpl.NEW_INSETS_MODE_FULL;
|
import static android.view.ViewRootImpl.NEW_INSETS_MODE_NONE;
|
import static android.view.WindowInsets.Type.MANDATORY_SYSTEM_GESTURES;
|
import static android.view.WindowInsets.Type.SIZE;
|
import static android.view.WindowInsets.Type.SYSTEM_GESTURES;
|
import static android.view.WindowInsets.Type.indexOf;
|
|
import android.annotation.IntDef;
|
import android.annotation.Nullable;
|
import android.graphics.Insets;
|
import android.graphics.Rect;
|
import android.os.Parcel;
|
import android.os.Parcelable;
|
import android.util.ArrayMap;
|
import android.util.ArraySet;
|
import android.util.SparseIntArray;
|
import android.view.WindowInsets.Type;
|
import android.view.WindowInsets.Type.InsetType;
|
import android.view.WindowManager.LayoutParams;
|
|
import java.io.PrintWriter;
|
import java.lang.annotation.Retention;
|
import java.lang.annotation.RetentionPolicy;
|
import java.util.Objects;
|
|
/**
|
* Holder for state of system windows that cause window insets for all other windows in the system.
|
* @hide
|
*/
|
public class InsetsState implements Parcelable {
|
|
/**
|
* Internal representation of inset source types. This is different from the public API in
|
* {@link WindowInsets.Type} as one type from the public API might indicate multiple windows
|
* at the same time.
|
*/
|
@Retention(RetentionPolicy.SOURCE)
|
@IntDef(prefix = "TYPE", value = {
|
TYPE_TOP_BAR,
|
TYPE_SIDE_BAR_1,
|
TYPE_SIDE_BAR_2,
|
TYPE_SIDE_BAR_3,
|
TYPE_TOP_GESTURES,
|
TYPE_BOTTOM_GESTURES,
|
TYPE_LEFT_GESTURES,
|
TYPE_RIGHT_GESTURES,
|
TYPE_TOP_TAPPABLE_ELEMENT,
|
TYPE_BOTTOM_TAPPABLE_ELEMENT,
|
TYPE_IME
|
})
|
public @interface InternalInsetType {}
|
|
static final int FIRST_TYPE = 0;
|
|
/** Top bar. Can be status bar or caption in freeform windowing mode. */
|
public static final int TYPE_TOP_BAR = FIRST_TYPE;
|
|
/**
|
* Up to 3 side bars that appear on left/right/bottom. On phones there is only one side bar
|
* (the navigation bar, see {@link #TYPE_NAVIGATION_BAR}), but other form factors might have
|
* multiple, like Android Auto.
|
*/
|
public static final int TYPE_SIDE_BAR_1 = 1;
|
public static final int TYPE_SIDE_BAR_2 = 2;
|
public static final int TYPE_SIDE_BAR_3 = 3;
|
|
public static final int TYPE_TOP_GESTURES = 4;
|
public static final int TYPE_BOTTOM_GESTURES = 5;
|
public static final int TYPE_LEFT_GESTURES = 6;
|
public static final int TYPE_RIGHT_GESTURES = 7;
|
public static final int TYPE_TOP_TAPPABLE_ELEMENT = 8;
|
public static final int TYPE_BOTTOM_TAPPABLE_ELEMENT = 9;
|
|
/** Input method window. */
|
public static final int TYPE_IME = 10;
|
|
static final int LAST_TYPE = TYPE_IME;
|
|
// Derived types
|
|
/** First side bar is navigation bar. */
|
public static final int TYPE_NAVIGATION_BAR = TYPE_SIDE_BAR_1;
|
|
/** A shelf is the same as the navigation bar. */
|
public static final int TYPE_SHELF = TYPE_NAVIGATION_BAR;
|
|
@Retention(RetentionPolicy.SOURCE)
|
@IntDef(prefix = "INSET_SIDE", value = {
|
INSET_SIDE_LEFT,
|
INSET_SIDE_TOP,
|
INSET_SIDE_RIGHT,
|
INSET_SIDE_BOTTOM,
|
INSET_SIDE_UNKNWON
|
})
|
public @interface InsetSide {}
|
static final int INSET_SIDE_LEFT = 0;
|
static final int INSET_SIDE_TOP = 1;
|
static final int INSET_SIDE_RIGHT = 2;
|
static final int INSET_SIDE_BOTTOM = 3;
|
static final int INSET_SIDE_UNKNWON = 4;
|
|
private final ArrayMap<Integer, InsetsSource> mSources = new ArrayMap<>();
|
|
/**
|
* The frame of the display these sources are relative to.
|
*/
|
private final Rect mDisplayFrame = new Rect();
|
|
public InsetsState() {
|
}
|
|
public InsetsState(InsetsState copy) {
|
set(copy);
|
}
|
|
public InsetsState(InsetsState copy, boolean copySources) {
|
set(copy, copySources);
|
}
|
|
/**
|
* Calculates {@link WindowInsets} based on the current source configuration.
|
*
|
* @param frame The frame to calculate the insets relative to.
|
* @return The calculated insets.
|
*/
|
public WindowInsets calculateInsets(Rect frame, boolean isScreenRound,
|
boolean alwaysConsumeSystemBars, DisplayCutout cutout,
|
@Nullable Rect legacyContentInsets, @Nullable Rect legacyStableInsets,
|
int legacySoftInputMode, @Nullable @InsetSide SparseIntArray typeSideMap) {
|
Insets[] typeInsetsMap = new Insets[Type.SIZE];
|
Insets[] typeMaxInsetsMap = new Insets[Type.SIZE];
|
boolean[] typeVisibilityMap = new boolean[SIZE];
|
final Rect relativeFrame = new Rect(frame);
|
final Rect relativeFrameMax = new Rect(frame);
|
if (ViewRootImpl.sNewInsetsMode != NEW_INSETS_MODE_FULL
|
&& legacyContentInsets != null && legacyStableInsets != null) {
|
WindowInsets.assignCompatInsets(typeInsetsMap, legacyContentInsets);
|
WindowInsets.assignCompatInsets(typeMaxInsetsMap, legacyStableInsets);
|
}
|
for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
|
InsetsSource source = mSources.get(type);
|
if (source == null) {
|
continue;
|
}
|
|
boolean skipSystemBars = ViewRootImpl.sNewInsetsMode != NEW_INSETS_MODE_FULL
|
&& (type == TYPE_TOP_BAR || type == TYPE_NAVIGATION_BAR);
|
boolean skipIme = source.getType() == TYPE_IME
|
&& (legacySoftInputMode & LayoutParams.SOFT_INPUT_ADJUST_RESIZE) == 0;
|
boolean skipLegacyTypes = ViewRootImpl.sNewInsetsMode == NEW_INSETS_MODE_NONE
|
&& (toPublicType(type) & Type.compatSystemInsets()) != 0;
|
if (skipSystemBars || skipIme || skipLegacyTypes) {
|
typeVisibilityMap[indexOf(toPublicType(type))] = source.isVisible();
|
continue;
|
}
|
|
processSource(source, relativeFrame, false /* ignoreVisibility */, typeInsetsMap,
|
typeSideMap, typeVisibilityMap);
|
|
// IME won't be reported in max insets as the size depends on the EditorInfo of the IME
|
// target.
|
if (source.getType() != TYPE_IME) {
|
processSource(source, relativeFrameMax, true /* ignoreVisibility */,
|
typeMaxInsetsMap, null /* typeSideMap */, null /* typeVisibilityMap */);
|
}
|
}
|
return new WindowInsets(typeInsetsMap, typeMaxInsetsMap, typeVisibilityMap, isScreenRound,
|
alwaysConsumeSystemBars, cutout);
|
}
|
|
private void processSource(InsetsSource source, Rect relativeFrame, boolean ignoreVisibility,
|
Insets[] typeInsetsMap, @Nullable @InsetSide SparseIntArray typeSideMap,
|
@Nullable boolean[] typeVisibilityMap) {
|
Insets insets = source.calculateInsets(relativeFrame, ignoreVisibility);
|
|
int type = toPublicType(source.getType());
|
processSourceAsPublicType(source, typeInsetsMap, typeSideMap, typeVisibilityMap,
|
insets, type);
|
|
if (type == MANDATORY_SYSTEM_GESTURES) {
|
// Mandatory system gestures are also system gestures.
|
// TODO: find a way to express this more generally. One option would be to define
|
// Type.systemGestureInsets() as NORMAL | MANDATORY, but then we lose the
|
// ability to set systemGestureInsets() independently from
|
// mandatorySystemGestureInsets() in the Builder.
|
processSourceAsPublicType(source, typeInsetsMap, typeSideMap, typeVisibilityMap,
|
insets, SYSTEM_GESTURES);
|
}
|
}
|
|
private void processSourceAsPublicType(InsetsSource source, Insets[] typeInsetsMap,
|
@InsetSide @Nullable SparseIntArray typeSideMap,
|
@Nullable boolean[] typeVisibilityMap, Insets insets, int type) {
|
int index = indexOf(type);
|
Insets existing = typeInsetsMap[index];
|
if (existing == null) {
|
typeInsetsMap[index] = insets;
|
} else {
|
typeInsetsMap[index] = Insets.max(existing, insets);
|
}
|
|
if (typeVisibilityMap != null) {
|
typeVisibilityMap[index] = source.isVisible();
|
}
|
|
if (typeSideMap != null && !Insets.NONE.equals(insets)) {
|
@InsetSide int insetSide = getInsetSide(insets);
|
if (insetSide != INSET_SIDE_UNKNWON) {
|
typeSideMap.put(source.getType(), getInsetSide(insets));
|
}
|
}
|
}
|
|
/**
|
* Retrieves the side for a certain {@code insets}. It is required that only one field l/t/r/b
|
* is set in order that this method returns a meaningful result.
|
*/
|
private @InsetSide int getInsetSide(Insets insets) {
|
if (insets.left != 0) {
|
return INSET_SIDE_LEFT;
|
}
|
if (insets.top != 0) {
|
return INSET_SIDE_TOP;
|
}
|
if (insets.right != 0) {
|
return INSET_SIDE_RIGHT;
|
}
|
if (insets.bottom != 0) {
|
return INSET_SIDE_BOTTOM;
|
}
|
return INSET_SIDE_UNKNWON;
|
}
|
|
public InsetsSource getSource(@InternalInsetType int type) {
|
return mSources.computeIfAbsent(type, InsetsSource::new);
|
}
|
|
public void setDisplayFrame(Rect frame) {
|
mDisplayFrame.set(frame);
|
}
|
|
public Rect getDisplayFrame() {
|
return mDisplayFrame;
|
}
|
|
/**
|
* Modifies the state of this class to exclude a certain type to make it ready for dispatching
|
* to the client.
|
*
|
* @param type The {@link InternalInsetType} of the source to remove
|
*/
|
public void removeSource(int type) {
|
mSources.remove(type);
|
}
|
|
public void set(InsetsState other) {
|
set(other, false /* copySources */);
|
}
|
|
public void set(InsetsState other, boolean copySources) {
|
mDisplayFrame.set(other.mDisplayFrame);
|
mSources.clear();
|
if (copySources) {
|
for (int i = 0; i < other.mSources.size(); i++) {
|
InsetsSource source = other.mSources.valueAt(i);
|
mSources.put(source.getType(), new InsetsSource(source));
|
}
|
} else {
|
mSources.putAll(other.mSources);
|
}
|
}
|
|
public void addSource(InsetsSource source) {
|
mSources.put(source.getType(), source);
|
}
|
|
public int getSourcesCount() {
|
return mSources.size();
|
}
|
|
public InsetsSource sourceAt(int index) {
|
return mSources.valueAt(index);
|
}
|
|
public static @InternalInsetType ArraySet<Integer> toInternalType(@InsetType int insetTypes) {
|
final ArraySet<Integer> result = new ArraySet<>();
|
if ((insetTypes & Type.TOP_BAR) != 0) {
|
result.add(TYPE_TOP_BAR);
|
}
|
if ((insetTypes & Type.SIDE_BARS) != 0) {
|
result.add(TYPE_SIDE_BAR_1);
|
result.add(TYPE_SIDE_BAR_2);
|
result.add(TYPE_SIDE_BAR_3);
|
}
|
if ((insetTypes & Type.IME) != 0) {
|
result.add(TYPE_IME);
|
}
|
return result;
|
}
|
|
static @InsetType int toPublicType(@InternalInsetType int type) {
|
switch (type) {
|
case TYPE_TOP_BAR:
|
return Type.TOP_BAR;
|
case TYPE_SIDE_BAR_1:
|
case TYPE_SIDE_BAR_2:
|
case TYPE_SIDE_BAR_3:
|
return Type.SIDE_BARS;
|
case TYPE_IME:
|
return Type.IME;
|
case TYPE_TOP_GESTURES:
|
case TYPE_BOTTOM_GESTURES:
|
return Type.MANDATORY_SYSTEM_GESTURES;
|
case TYPE_LEFT_GESTURES:
|
case TYPE_RIGHT_GESTURES:
|
return Type.SYSTEM_GESTURES;
|
case TYPE_TOP_TAPPABLE_ELEMENT:
|
case TYPE_BOTTOM_TAPPABLE_ELEMENT:
|
return Type.TAPPABLE_ELEMENT;
|
default:
|
throw new IllegalArgumentException("Unknown type: " + type);
|
}
|
}
|
|
public static boolean getDefaultVisibility(@InsetType int type) {
|
switch (type) {
|
case TYPE_TOP_BAR:
|
case TYPE_SIDE_BAR_1:
|
case TYPE_SIDE_BAR_2:
|
case TYPE_SIDE_BAR_3:
|
return true;
|
case TYPE_IME:
|
return false;
|
default:
|
return true;
|
}
|
}
|
|
public void dump(String prefix, PrintWriter pw) {
|
pw.println(prefix + "InsetsState");
|
for (int i = mSources.size() - 1; i >= 0; i--) {
|
mSources.valueAt(i).dump(prefix + " ", pw);
|
}
|
}
|
|
public static String typeToString(int type) {
|
switch (type) {
|
case TYPE_TOP_BAR:
|
return "TYPE_TOP_BAR";
|
case TYPE_SIDE_BAR_1:
|
return "TYPE_SIDE_BAR_1";
|
case TYPE_SIDE_BAR_2:
|
return "TYPE_SIDE_BAR_2";
|
case TYPE_SIDE_BAR_3:
|
return "TYPE_SIDE_BAR_3";
|
case TYPE_TOP_GESTURES:
|
return "TYPE_TOP_GESTURES";
|
case TYPE_BOTTOM_GESTURES:
|
return "TYPE_BOTTOM_GESTURES";
|
case TYPE_LEFT_GESTURES:
|
return "TYPE_LEFT_GESTURES";
|
case TYPE_RIGHT_GESTURES:
|
return "TYPE_RIGHT_GESTURES";
|
case TYPE_TOP_TAPPABLE_ELEMENT:
|
return "TYPE_TOP_TAPPABLE_ELEMENT";
|
case TYPE_BOTTOM_TAPPABLE_ELEMENT:
|
return "TYPE_BOTTOM_TAPPABLE_ELEMENT";
|
default:
|
return "TYPE_UNKNOWN_" + type;
|
}
|
}
|
|
@Override
|
public boolean equals(Object o) {
|
if (this == o) { return true; }
|
if (o == null || getClass() != o.getClass()) { return false; }
|
|
InsetsState state = (InsetsState) o;
|
|
if (!mDisplayFrame.equals(state.mDisplayFrame)) {
|
return false;
|
}
|
if (mSources.size() != state.mSources.size()) {
|
return false;
|
}
|
for (int i = mSources.size() - 1; i >= 0; i--) {
|
InsetsSource source = mSources.valueAt(i);
|
InsetsSource otherSource = state.mSources.get(source.getType());
|
if (otherSource == null) {
|
return false;
|
}
|
if (!otherSource.equals(source)) {
|
return false;
|
}
|
}
|
return true;
|
}
|
|
@Override
|
public int hashCode() {
|
return Objects.hash(mDisplayFrame, mSources);
|
}
|
|
public InsetsState(Parcel in) {
|
readFromParcel(in);
|
}
|
|
@Override
|
public int describeContents() {
|
return 0;
|
}
|
|
@Override
|
public void writeToParcel(Parcel dest, int flags) {
|
dest.writeParcelable(mDisplayFrame, flags);
|
dest.writeInt(mSources.size());
|
for (int i = 0; i < mSources.size(); i++) {
|
dest.writeParcelable(mSources.valueAt(i), flags);
|
}
|
}
|
|
public static final @android.annotation.NonNull Creator<InsetsState> CREATOR = new Creator<InsetsState>() {
|
|
public InsetsState createFromParcel(Parcel in) {
|
return new InsetsState(in);
|
}
|
|
public InsetsState[] newArray(int size) {
|
return new InsetsState[size];
|
}
|
};
|
|
public void readFromParcel(Parcel in) {
|
mSources.clear();
|
mDisplayFrame.set(in.readParcelable(null /* loader */));
|
final int size = in.readInt();
|
for (int i = 0; i < size; i++) {
|
final InsetsSource source = in.readParcelable(null /* loader */);
|
mSources.put(source.getType(), source);
|
}
|
}
|
}
|