/* * 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 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 toInternalType(@InsetType int insetTypes) { final ArraySet 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 CREATOR = new Creator() { 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); } } }