fix: edge handling

This commit is contained in:
Osei Fortune
2025-11-04 13:13:49 -04:00
parent 93f3578677
commit ce20233674
7 changed files with 369 additions and 509 deletions

View File

@@ -26,20 +26,21 @@ public abstract class LayoutBase extends ViewGroup {
private boolean passThroughParent;
boolean applyingEdges;
public static final int OverflowEdgeIgnore = -1;
public static final int OverflowEdgeNone = 0;
public static final int OverflowEdgeLeft = 1;
public static final int OverflowEdgeTop = 1 << 1;
public static final int OverflowEdgeRight = 1 << 2;
public static final int OverflowEdgeBottom = 1 << 3;
public static final int OverflowEdgeDontApply = 1 << 4;
public static final int OverflowEdgeLeftDontConsume = 1 << 5;
public static final int OverflowEdgeTopDontConsume = 1 << 6;
public static final int OverflowEdgeRightDontConsume = 1 << 7;
public static final int OverflowEdgeBottomDontConsume = 1 << 8;
public static final int OverflowEdgeAllButLeft = 1 << 9;
public static final int OverflowEdgeAllButTop = 1 << 10;
public static final int OverflowEdgeAllButRight = 1 << 11;
public static final int OverflowEdgeAllButBottom = 1 << 12;
public static final int OverflowEdgeLeft = 1 << 1;
public static final int OverflowEdgeTop = 1 << 2;
public static final int OverflowEdgeRight = 1 << 3;
public static final int OverflowEdgeBottom = 1 << 4;
public static final int OverflowEdgeDontApply = 1 << 5;
public static final int OverflowEdgeLeftDontConsume = 1 << 6;
public static final int OverflowEdgeTopDontConsume = 1 << 7;
public static final int OverflowEdgeRightDontConsume = 1 << 8;
public static final int OverflowEdgeBottomDontConsume = 1 << 9;
public static final int OverflowEdgeAllButLeft = 1 << 10;
public static final int OverflowEdgeAllButTop = 1 << 11;
public static final int OverflowEdgeAllButRight = 1 << 12;
public static final int OverflowEdgeAllButBottom = 1 << 13;
public static final class BufferOffset {
public static final int INSET_LEFT = 0;
@@ -53,8 +54,6 @@ public abstract class LayoutBase extends ViewGroup {
public static final int INSET_BOTTOM_CONSUMED = 28;
}
;
int mPaddingLeft = 0;
int mPaddingTop = 0;
int mPaddingRight = 0;
@@ -62,11 +61,12 @@ public abstract class LayoutBase extends ViewGroup {
Insets edgeInsets = Insets.NONE;
int overflowEdge = OverflowEdgeNone;
int overflowEdge = OverflowEdgeIgnore;
private final ByteBuffer insetBuffer = ByteBuffer.allocateDirect(32);
private WindowInsetListener insetListener = null;
private androidx.core.view.OnApplyWindowInsetsListener windowInsetsListener = null;
public void setInsetListener(@Nullable WindowInsetListener insetListener) {
this.insetListener = insetListener;
@@ -98,199 +98,6 @@ public abstract class LayoutBase extends ViewGroup {
public LayoutBase(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
insetBuffer.order(ByteOrder.nativeOrder());
// if incoming inset is empty and previous inset is empty return consumed
// an incoming empty inset is one way to detect a consumed inset e.g multiple views consumed top/bottom
androidx.core.view.OnApplyWindowInsetsListener windowInsetsListener = new androidx.core.view.OnApplyWindowInsetsListener() {
@NonNull
@Override
public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) {
if (insets.isConsumed()) {
return insets;
}
if (v instanceof LayoutBase) {
LayoutBase base = (LayoutBase) v;
Insets statusBar = insets.getInsets(WindowInsetsCompat.Type.statusBars());
Insets navBar = insets.getInsets(WindowInsetsCompat.Type.navigationBars());
Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime());
int insetLeft = navBar.left;
int insetRight = navBar.right;
int insetBottom = Math.max(navBar.bottom, ime.bottom);
insetBuffer.put(EMPTY_INSETS, 0, 32);
insetBuffer.rewind();
if (overflowEdge == OverflowEdgeNone) {
base.applyingEdges = true;
v.setPadding(mPaddingLeft + insetLeft, mPaddingTop + statusBar.top, mPaddingRight + insetRight, mPaddingBottom + insetBottom);
edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom);
base.applyingEdges = false;
return WindowInsetsCompat.CONSUMED;
}
if (base.insetListener != null) {
if (overflowEdge == OverflowEdgeDontApply) {
// if incoming inset is empty and previous inset is empty return consumed
// an incoming empty inset is one way to detect a consumed inset e.g multiple views consumed top/bottom
if (Insets.NONE.equals(statusBar) && Insets.NONE.equals(navBar) && Insets.NONE.equals(ime) && Insets.NONE.equals(edgeInsets)) {
return WindowInsetsCompat.CONSUMED;
}
IntBuffer insetData = insetBuffer.asIntBuffer();
boolean leftPreviouslyConsumed = insetLeft == 0;
boolean topPreviouslyConsumed = statusBar.top == 0;
boolean rightPreviouslyConsumed = insetRight == 0;
boolean bottomPreviouslyConsumed = insetBottom == 0;
insetData.put(0, insetLeft).put(1, statusBar.top).put(2, insetRight).put(3, insetBottom).put(4, leftPreviouslyConsumed ? 1 : 0).put(5, topPreviouslyConsumed ? 1 : 0).put(6, rightPreviouslyConsumed ? 1 : 0).put(7, bottomPreviouslyConsumed ? 1 : 0);
base.insetListener.onApplyWindowInsets(insetBuffer);
int leftInset = insetData.get(0);
int topInset = insetData.get(1);
int rightInset = insetData.get(2);
int bottomInset = insetData.get(3);
boolean leftConsumed = insetData.get(4) > 0;
boolean topConsumed = insetData.get(5) > 0;
boolean rightConsumed = insetData.get(6) > 0;
boolean bottomConsumed = insetData.get(7) > 0;
if (leftConsumed && topConsumed && rightConsumed && bottomConsumed) {
edgeInsets = Insets.of(leftInset, topInset, rightInset, bottomInset);
base.setPadding(leftInset, topInset, rightInset, bottomInset);
return new WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE).build();
}
base.setPadding(leftPreviouslyConsumed ? 0 : leftInset, topPreviouslyConsumed ? 0 : topInset, rightPreviouslyConsumed ? 0 : rightInset, bottomPreviouslyConsumed ? 0 : bottomInset);
// restore inset edge if not consumed
if (!(leftPreviouslyConsumed || leftConsumed)) {
leftInset = insetLeft;
}
if (!(topPreviouslyConsumed || topConsumed)) {
topInset = statusBar.top;
}
if (!(rightPreviouslyConsumed || rightConsumed)) {
rightInset = insetRight;
}
if (!(bottomPreviouslyConsumed || bottomConsumed)) {
bottomInset = insetBottom;
}
edgeInsets = Insets.of(leftPreviouslyConsumed ? 0 : leftInset, topPreviouslyConsumed ? 0 : topInset, rightPreviouslyConsumed ? 0 : rightInset, bottomPreviouslyConsumed ? 0 : bottomInset);
return new WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(), Insets.of(leftPreviouslyConsumed || leftConsumed ? 0 : leftInset, topPreviouslyConsumed || topConsumed ? 0 : topInset, rightPreviouslyConsumed || rightConsumed ? 0 : rightInset, bottomPreviouslyConsumed || bottomConsumed ? 0 : bottomInset)).build();
}
}
boolean overflowLeftConsume = (overflowEdge & OverflowEdgeLeft) == OverflowEdgeLeft;
boolean overflowTopConsume = (overflowEdge & OverflowEdgeTop) == OverflowEdgeTop;
boolean overflowRightConsume = (overflowEdge & OverflowEdgeRight) == OverflowEdgeRight;
boolean overflowBottomConsume = (overflowEdge & OverflowEdgeBottom) == OverflowEdgeBottom;
boolean overflowLeft = (overflowEdge & OverflowEdgeLeftDontConsume) == OverflowEdgeLeftDontConsume;
boolean overflowTop = (overflowEdge & OverflowEdgeTopDontConsume) == OverflowEdgeTopDontConsume;
boolean overflowRight = (overflowEdge & OverflowEdgeRightDontConsume) == OverflowEdgeRightDontConsume;
boolean overflowBottom = (overflowEdge & OverflowEdgeBottomDontConsume) == OverflowEdgeBottomDontConsume;
boolean overflowAllButLeft = (overflowEdge & OverflowEdgeAllButLeft) == OverflowEdgeAllButLeft;
boolean overflowAllButTop = (overflowEdge & OverflowEdgeAllButTop) == OverflowEdgeAllButTop;
boolean overflowAllButRight = (overflowEdge & OverflowEdgeAllButRight) == OverflowEdgeAllButRight;
boolean overflowAllButBottom = (overflowEdge & OverflowEdgeAllButBottom) == OverflowEdgeAllButBottom;
WindowInsetsCompat ret = insets;
base.applyingEdges = true;
int left = 0;
int top = 0;
int right = 0;
int bottom = 0;
if (overflowAllButLeft || overflowAllButTop || overflowAllButRight || overflowAllButBottom) {
Insets newInset;
if (overflowAllButLeft) {
left = mPaddingLeft + insetLeft;
edgeInsets = Insets.of(insetLeft, 0, 0, 0);
newInset = Insets.of(0, statusBar.top, insetRight, insetBottom);
} else if (overflowAllButTop) {
top = mPaddingTop + statusBar.top;
edgeInsets = Insets.of(0, statusBar.top, 0, 0);
newInset = Insets.of(insetLeft, 0, insetRight, insetBottom);
} else if (overflowAllButRight) {
right = mPaddingRight + insetRight;
edgeInsets = Insets.of(0, 0, insetRight, 0);
newInset = Insets.of(insetLeft, statusBar.top, 0, insetBottom);
} else {
bottom = mPaddingBottom + insetBottom;
edgeInsets = Insets.of(0, 0, 0, insetBottom);
newInset = Insets.of(insetLeft, statusBar.top, insetRight, 0);
}
ret = new WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(), newInset).build();
base.setPadding(left, top, right, bottom);
base.applyingEdges = false;
if (newInset == Insets.NONE) {
return WindowInsetsCompat.CONSUMED;
}
return ret;
}
if (overflowLeftConsume || overflowLeft) {
top = mPaddingTop + statusBar.top;
right = mPaddingRight + insetRight;
bottom = mPaddingBottom + insetBottom;
edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom);
if (overflowRightConsume) {
ret = WindowInsetsCompat.CONSUMED;
}
}
if (overflowTopConsume || overflowTop) {
left = mPaddingLeft + insetLeft;
right = mPaddingRight + insetRight;
bottom = mPaddingBottom + insetBottom;
edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom);
if (overflowTopConsume) {
ret = WindowInsetsCompat.CONSUMED;
}
}
if (overflowRightConsume || overflowRight) {
left = mPaddingLeft + insetLeft;
top = mPaddingTop + statusBar.top;
bottom = mPaddingBottom + insetBottom;
edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom);
if (overflowRightConsume) {
ret = WindowInsetsCompat.CONSUMED;
}
}
if (overflowBottomConsume || overflowBottom) {
left = mPaddingLeft + insetLeft;
top = mPaddingTop + statusBar.top;
right = mPaddingRight + insetRight;
edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom);
if (overflowBottomConsume) {
ret = WindowInsetsCompat.CONSUMED;
}
}
base.setPadding(left, top, right, bottom);
base.applyingEdges = false;
return ret;
}
return insets;
}
};
ViewCompat.setOnApplyWindowInsetsListener(this, windowInsetsListener);
}
@Override
@@ -300,7 +107,6 @@ public abstract class LayoutBase extends ViewGroup {
public LayoutBase(Context context) {
super(context);
}
public Insets getEdgeInsets() {
@@ -394,6 +200,210 @@ public abstract class LayoutBase extends ViewGroup {
public void setOverflowEdge(int value) {
overflowEdge = value;
if (value == OverflowEdgeIgnore) {
ViewCompat.setOnApplyWindowInsetsListener(this, null);
} else if (windowInsetsListener == null) {
// if incoming inset is empty and previous inset is empty return consumed
// an incoming empty inset is one way to detect a consumed inset e.g multiple views consumed top/bottom
windowInsetsListener = new androidx.core.view.OnApplyWindowInsetsListener() {
@NonNull
@Override
public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat insets) {
if (insets.isConsumed()) {
return insets;
}
if (v instanceof LayoutBase) {
LayoutBase base = (LayoutBase) v;
// should not occur but if it does return the inset
if (overflowEdge == OverflowEdgeIgnore) {
return insets;
}
Insets statusBar = insets.getInsets(WindowInsetsCompat.Type.statusBars());
Insets navBar = insets.getInsets(WindowInsetsCompat.Type.navigationBars());
Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime());
int insetLeft = navBar.left;
int insetRight = navBar.right;
int insetBottom = Math.max(navBar.bottom, ime.bottom);
insetBuffer.put(EMPTY_INSETS, 0, 32);
insetBuffer.rewind();
if (overflowEdge == OverflowEdgeNone) {
base.applyingEdges = true;
v.setPadding(mPaddingLeft + insetLeft, mPaddingTop + statusBar.top, mPaddingRight + insetRight, mPaddingBottom + insetBottom);
edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom);
base.applyingEdges = false;
return WindowInsetsCompat.CONSUMED;
}
if (base.insetListener != null) {
if (overflowEdge == OverflowEdgeDontApply) {
// if incoming inset is empty and previous inset is empty return consumed
// an incoming empty inset is one way to detect a consumed inset e.g multiple views consumed top/bottom
if (Insets.NONE.equals(statusBar) && Insets.NONE.equals(navBar) && Insets.NONE.equals(ime) && Insets.NONE.equals(edgeInsets)) {
return WindowInsetsCompat.CONSUMED;
}
IntBuffer insetData = insetBuffer.asIntBuffer();
boolean leftPreviouslyConsumed = insetLeft == 0;
boolean topPreviouslyConsumed = statusBar.top == 0;
boolean rightPreviouslyConsumed = insetRight == 0;
boolean bottomPreviouslyConsumed = insetBottom == 0;
insetData.put(0, insetLeft).put(1, statusBar.top).put(2, insetRight).put(3, insetBottom).put(4, leftPreviouslyConsumed ? 1 : 0).put(5, topPreviouslyConsumed ? 1 : 0).put(6, rightPreviouslyConsumed ? 1 : 0).put(7, bottomPreviouslyConsumed ? 1 : 0);
base.insetListener.onApplyWindowInsets(insetBuffer);
int leftInset = insetData.get(0);
int topInset = insetData.get(1);
int rightInset = insetData.get(2);
int bottomInset = insetData.get(3);
boolean leftConsumed = insetData.get(4) > 0;
boolean topConsumed = insetData.get(5) > 0;
boolean rightConsumed = insetData.get(6) > 0;
boolean bottomConsumed = insetData.get(7) > 0;
if (leftConsumed && topConsumed && rightConsumed && bottomConsumed) {
edgeInsets = Insets.of(leftInset, topInset, rightInset, bottomInset);
base.setPadding(leftInset, topInset, rightInset, bottomInset);
return new WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE).build();
}
base.setPadding(leftPreviouslyConsumed ? 0 : leftInset, topPreviouslyConsumed ? 0 : topInset, rightPreviouslyConsumed ? 0 : rightInset, bottomPreviouslyConsumed ? 0 : bottomInset);
// restore inset edge if not consumed
if (!(leftPreviouslyConsumed || leftConsumed)) {
leftInset = insetLeft;
}
if (!(topPreviouslyConsumed || topConsumed)) {
topInset = statusBar.top;
}
if (!(rightPreviouslyConsumed || rightConsumed)) {
rightInset = insetRight;
}
if (!(bottomPreviouslyConsumed || bottomConsumed)) {
bottomInset = insetBottom;
}
edgeInsets = Insets.of(leftPreviouslyConsumed ? 0 : leftInset, topPreviouslyConsumed ? 0 : topInset, rightPreviouslyConsumed ? 0 : rightInset, bottomPreviouslyConsumed ? 0 : bottomInset);
return new WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(), Insets.of(leftPreviouslyConsumed || leftConsumed ? 0 : leftInset, topPreviouslyConsumed || topConsumed ? 0 : topInset, rightPreviouslyConsumed || rightConsumed ? 0 : rightInset, bottomPreviouslyConsumed || bottomConsumed ? 0 : bottomInset)).build();
}
}
boolean overflowLeftConsume = (overflowEdge & OverflowEdgeLeft) == OverflowEdgeLeft;
boolean overflowTopConsume = (overflowEdge & OverflowEdgeTop) == OverflowEdgeTop;
boolean overflowRightConsume = (overflowEdge & OverflowEdgeRight) == OverflowEdgeRight;
boolean overflowBottomConsume = (overflowEdge & OverflowEdgeBottom) == OverflowEdgeBottom;
boolean overflowLeft = (overflowEdge & OverflowEdgeLeftDontConsume) == OverflowEdgeLeftDontConsume;
boolean overflowTop = (overflowEdge & OverflowEdgeTopDontConsume) == OverflowEdgeTopDontConsume;
boolean overflowRight = (overflowEdge & OverflowEdgeRightDontConsume) == OverflowEdgeRightDontConsume;
boolean overflowBottom = (overflowEdge & OverflowEdgeBottomDontConsume) == OverflowEdgeBottomDontConsume;
boolean overflowAllButLeft = (overflowEdge & OverflowEdgeAllButLeft) == OverflowEdgeAllButLeft;
boolean overflowAllButTop = (overflowEdge & OverflowEdgeAllButTop) == OverflowEdgeAllButTop;
boolean overflowAllButRight = (overflowEdge & OverflowEdgeAllButRight) == OverflowEdgeAllButRight;
boolean overflowAllButBottom = (overflowEdge & OverflowEdgeAllButBottom) == OverflowEdgeAllButBottom;
WindowInsetsCompat ret = insets;
base.applyingEdges = true;
int left = 0;
int top = 0;
int right = 0;
int bottom = 0;
if (overflowAllButLeft || overflowAllButTop || overflowAllButRight || overflowAllButBottom) {
Insets newInset;
if (overflowAllButLeft) {
left = mPaddingLeft + insetLeft;
edgeInsets = Insets.of(insetLeft, 0, 0, 0);
newInset = Insets.of(0, statusBar.top, insetRight, insetBottom);
} else if (overflowAllButTop) {
top = mPaddingTop + statusBar.top;
edgeInsets = Insets.of(0, statusBar.top, 0, 0);
newInset = Insets.of(insetLeft, 0, insetRight, insetBottom);
} else if (overflowAllButRight) {
right = mPaddingRight + insetRight;
edgeInsets = Insets.of(0, 0, insetRight, 0);
newInset = Insets.of(insetLeft, statusBar.top, 0, insetBottom);
} else {
bottom = mPaddingBottom + insetBottom;
edgeInsets = Insets.of(0, 0, 0, insetBottom);
newInset = Insets.of(insetLeft, statusBar.top, insetRight, 0);
}
ret = new WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(), newInset).build();
base.setPadding(left, top, right, bottom);
base.applyingEdges = false;
if (newInset == Insets.NONE) {
return WindowInsetsCompat.CONSUMED;
}
return ret;
}
if (overflowLeftConsume || overflowLeft) {
top = mPaddingTop + statusBar.top;
right = mPaddingRight + insetRight;
bottom = mPaddingBottom + insetBottom;
edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom);
if (overflowRightConsume) {
ret = WindowInsetsCompat.CONSUMED;
}
}
if (overflowTopConsume || overflowTop) {
left = mPaddingLeft + insetLeft;
right = mPaddingRight + insetRight;
bottom = mPaddingBottom + insetBottom;
edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom);
if (overflowTopConsume) {
ret = WindowInsetsCompat.CONSUMED;
}
}
if (overflowRightConsume || overflowRight) {
left = mPaddingLeft + insetLeft;
top = mPaddingTop + statusBar.top;
bottom = mPaddingBottom + insetBottom;
edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom);
if (overflowRightConsume) {
ret = WindowInsetsCompat.CONSUMED;
}
}
if (overflowBottomConsume || overflowBottom) {
left = mPaddingLeft + insetLeft;
top = mPaddingTop + statusBar.top;
right = mPaddingRight + insetRight;
edgeInsets = Insets.of(insetLeft, statusBar.top, insetRight, insetBottom);
if (overflowBottomConsume) {
ret = WindowInsetsCompat.CONSUMED;
}
}
base.setPadding(left, top, right, bottom);
base.applyingEdges = false;
return ret;
}
return insets;
}
};
ViewCompat.setOnApplyWindowInsetsListener(this, windowInsetsListener);
}
if (pendingInsetApply) {
return;
}