feat(android): edge-to-edge

Squashed from feat/edge-to-edge and resolved conflicts
This commit is contained in:
Nathan Walker
2025-09-02 09:19:58 -07:00
parent a12fe192d5
commit 84e9190f87
57 changed files with 1914 additions and 260 deletions

View File

@@ -1,11 +1,11 @@
package org.nativescript.widgets;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.View.MeasureSpec;
import android.util.AttributeSet;
import org.json.JSONArray;
import org.json.JSONException;
@@ -29,18 +29,22 @@ public class GridLayout extends LayoutBase {
private final HashMap<View, MeasureSpecs> map = new HashMap<>();
public GridLayout(Context context) {
this(context, (AttributeSet)null);
this(context, (AttributeSet) null);
}
public GridLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public GridLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public GridLayout(Context context, String rows) {
this(context);
this.addRowsFromJSON(rows);
}
public GridLayout(Context context, String rows, String columns) {
this(context, rows);
this.addColumnsFromJSON(rows);
@@ -92,7 +96,7 @@ public class GridLayout extends LayoutBase {
return;
}
JSONArray rows = new JSONArray(value);
for (int i = 0; i < rows.length() ; i++) {
for (int i = 0; i < rows.length(); i++) {
JSONObject row = rows.getJSONObject(i);
addRow(row.getInt("value"), GridUnitType.values()[row.getInt("type")]);
}
@@ -101,13 +105,14 @@ public class GridLayout extends LayoutBase {
exception.printStackTrace();
}
}
public void addColumnsFromJSON(String value) {
try {
if (value == null) {
return;
}
JSONArray columns = new JSONArray(value);
for (int i = 0; i < columns.length() ; i++) {
for (int i = 0; i < columns.length(); i++) {
JSONObject column = columns.getJSONObject(i);
addColumn(column.getInt("value"), GridUnitType.values()[column.getInt("type")]);
}
@@ -116,6 +121,7 @@ public class GridLayout extends LayoutBase {
exception.printStackTrace();
}
}
public void addRowsAndColumnsFromJSON(String rowsString, String jsonString) {
addRowsFromJSON(rowsString);
addColumnsFromJSON(jsonString);
@@ -223,7 +229,7 @@ public class GridLayout extends LayoutBase {
}
private ItemSpec getColumnSpec(CommonLayoutParams lp) {
if (this._cols.size() == 0) {
if (this._cols.isEmpty()) {
return this.helper.singleColumn;
}
@@ -232,7 +238,7 @@ public class GridLayout extends LayoutBase {
}
private ItemSpec getRowSpec(CommonLayoutParams lp) {
if (this._rows.size() == 0) {
if (this._rows.isEmpty()) {
return this.helper.singleRow;
}
@@ -241,7 +247,7 @@ public class GridLayout extends LayoutBase {
}
private int getColumnSpan(CommonLayoutParams lp, int columnIndex) {
if (this._cols.size() == 0) {
if (this._cols.isEmpty()) {
return 1;
}
@@ -249,7 +255,7 @@ public class GridLayout extends LayoutBase {
}
private int getRowSpan(CommonLayoutParams lp, int rowIndex) {
if (this._rows.size() == 0) {
if (this._rows.isEmpty()) {
return 1;
}
@@ -344,8 +350,10 @@ public class GridLayout extends LayoutBase {
}
MeasureSpecs measureSpecs = this.map.get(child);
this.updateMeasureSpecs(child, measureSpecs);
this.helper.addMeasureSpec(measureSpecs);
if (measureSpecs != null) {
this.updateMeasureSpecs(child, measureSpecs);
this.helper.addMeasureSpec(measureSpecs);
}
}
this.helper.measure();
@@ -1145,7 +1153,7 @@ class MeasureHelper {
}
if (remainingSpace > 0) {
this.minRowStarValue = Math.max(remainingSpace / measureSpec.starRowsCount, this.minRowStarValue);
this.minRowStarValue = Math.max((float) remainingSpace / measureSpec.starRowsCount, this.minRowStarValue);
}
}
}
@@ -1165,7 +1173,7 @@ class MeasureHelper {
}
if (remainingSpace > 0) {
this.minColumnStarValue = Math.max(remainingSpace / measureSpec.starColumnsCount, this.minColumnStarValue);
this.minColumnStarValue = Math.max((float) remainingSpace / measureSpec.starColumnsCount, this.minColumnStarValue);
}
}
}

View File

@@ -6,20 +6,305 @@ import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
/**
* @author hhristov
*/
public abstract class LayoutBase extends ViewGroup {
private boolean passThroughParent;
boolean applyingEdges;
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 class BufferOffset {
public static final int INSET_LEFT = 0;
public static final int INSET_TOP = 4;
public static final int INSET_RIGHT = 8;
public static final int INSET_BOTTOM = 12;
public static final int INSET_LEFT_CONSUMED = 16;
public static final int INSET_TOP_CONSUMED = 20;
public static final int INSET_RIGHT_CONSUMED = 24;
public static final int INSET_BOTTOM_CONSUMED = 28;
}
;
int mPaddingLeft = 0;
int mPaddingTop = 0;
int mPaddingRight = 0;
int mPaddingBottom = 0;
Insets edgeInsets = Insets.NONE;
int overflowEdge = OverflowEdgeNone;
private final ByteBuffer insetBuffer = ByteBuffer.allocateDirect(32);
private WindowInsetListener insetListener = null;
public void setInsetListener(@Nullable WindowInsetListener insetListener) {
this.insetListener = insetListener;
}
public interface WindowInsetListener {
void onApplyWindowInsets(ByteBuffer inset);
}
private static final byte[] EMPTY_INSETS = new byte[32];
private boolean pendingInsetApply = false;
private final OnAttachStateChangeListener onAttachStateChangeListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(@NonNull View v) {
if (pendingInsetApply) {
pendingInsetApply = false;
removeOnAttachStateChangeListener(onAttachStateChangeListener);
ViewCompat.requestApplyInsets(v);
}
}
@Override
public void onViewDetachedFromWindow(@NonNull View v) {
}
};
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
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
return super.onApplyWindowInsets(insets);
}
public LayoutBase(Context context) {
super(context);
}
public Insets getEdgeInsets() {
return edgeInsets;
}
@Override
@@ -95,4 +380,32 @@ public abstract class LayoutBase extends ViewGroup {
public void setPassThroughParent(boolean value) {
this.passThroughParent = value;
}
@Override
public void setPadding(int left, int top, int right, int bottom) {
if (!applyingEdges) {
mPaddingLeft = left;
mPaddingTop = top;
mPaddingRight = right;
mPaddingBottom = bottom;
}
super.setPadding(left, top, right, bottom);
}
public void setOverflowEdge(int value) {
overflowEdge = value;
if (pendingInsetApply) {
return;
}
if (isAttachedToWindow()) {
ViewCompat.requestApplyInsets(this);
} else {
pendingInsetApply = true;
addOnAttachStateChangeListener(onAttachStateChangeListener);
}
}
public int getOverflowEdge() {
return overflowEdge;
}
}

View File

@@ -2,6 +2,7 @@ package org.nativescript.widgets;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
@@ -23,6 +24,9 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.activity.ComponentActivity;
import androidx.activity.SystemBarStyle;
import androidx.annotation.ColorInt;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.view.ViewCompat;
import androidx.exifinterface.media.ExifInterface;
@@ -38,8 +42,53 @@ import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class Utils {
public interface HandleDarkMode {
boolean onHandle(int bar, Resources resources);
}
enum HandleDarkModeBar {
status(0),
navigation(1);
private final int mValue;
HandleDarkModeBar(int i) {
this.mValue = i;
}
public int getValue() {
return this.mValue;
}
}
// The light scrim color used in the platform API 29+
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/policy/DecorView.java;drc=6ef0f022c333385dba2c294e35b8de544455bf19;l=142
static final int DefaultLightScrim = Color.argb(0xe6, 0xFF, 0xFF, 0xFF);
// The dark scrim color used in the platform.
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/res/res/color/system_bar_background_semi_transparent.xml
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/res/remote_color_resources_res/values/colors.xml;l=67
static final int DefaultDarkScrim = Color.argb(0x80, 0x1b, 0x1b, 0x1b);
public static void enableEdgeToEdge(ComponentActivity activity) {
androidx.activity.EdgeToEdge.enable(activity);
}
public static void enableEdgeToEdge(ComponentActivity activity, HandleDarkMode handleDarkMode) {
androidx.activity.EdgeToEdge.enable(activity, SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT, resources -> handleDarkMode.onHandle(HandleDarkModeBar.status.getValue(), resources)), SystemBarStyle.auto(DefaultLightScrim, DefaultDarkScrim, resources -> handleDarkMode.onHandle(HandleDarkModeBar.navigation.getValue(), resources)));
}
public static void enableEdgeToEdge(ComponentActivity activity, @ColorInt Integer statusBarLight, @ColorInt Integer statusBarDark, @ColorInt Integer navigationBarLight, @ColorInt Integer navigationBarDark) {
androidx.activity.EdgeToEdge.enable(activity, SystemBarStyle.auto(statusBarLight, statusBarDark), SystemBarStyle.auto(navigationBarLight, navigationBarDark));
}
public static void enableEdgeToEdge(ComponentActivity activity, @ColorInt Integer statusBarLight, @ColorInt Integer statusBarDark, @ColorInt Integer navigationBarLight, @ColorInt Integer navigationBarDark, HandleDarkMode handleDarkMode) {
androidx.activity.EdgeToEdge.enable(activity, SystemBarStyle.auto(statusBarLight, statusBarDark, resources -> handleDarkMode.onHandle(HandleDarkModeBar.status.getValue(), resources)), SystemBarStyle.auto(navigationBarLight, navigationBarDark, resources -> handleDarkMode.onHandle(HandleDarkModeBar.navigation.getValue(), resources)));
}
public static Drawable getDrawable(String uri, Context context) {
int resId = 0;
int resPrefixLength = "res://".length();
@@ -426,10 +475,7 @@ public class Utils {
Bitmap.CompressFormat targetFormat = getTargetFormat(format);
try (
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Base64OutputStream base64Stream = new Base64OutputStream(outputStream, android.util.Base64.NO_WRAP)
) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); Base64OutputStream base64Stream = new Base64OutputStream(outputStream, android.util.Base64.NO_WRAP)) {
bitmap.compress(targetFormat, quality, base64Stream);
result = outputStream.toString();
} catch (Exception e) {
@@ -460,9 +506,7 @@ public class Utils {
return new Pair<>((int) width, (int) height);
}
return new Pair<>(
Math.round((maxSize * width) / height)
, (int) maxSize);
return new Pair<>(Math.round((maxSize * width) / height), (int) maxSize);
}
if (width <= maxSize) {