Merge "Synchronize adding and removing table trackers." into androidx-main
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
index a57afce..e5a2b92 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
@@ -352,7 +352,8 @@
fun testLifecycleCallback_whenDestroyed() {
val lifecycleOnBackPressedCallback = CountingOnBackPressedCallback()
- val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.DESTROYED)
+ val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.CREATED)
+ lifecycleOwner.lifecycle.currentState = Lifecycle.State.DESTROYED
dispatcher.addCallback(lifecycleOwner, lifecycleOnBackPressedCallback)
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/utils/DateTimeFormatValidatorTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/utils/DateTimeFormatValidatorTest.java
index cf76375..df1b190 100644
--- a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/utils/DateTimeFormatValidatorTest.java
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/utils/DateTimeFormatValidatorTest.java
@@ -44,6 +44,12 @@
}
@Test
+ public void testValidateISO8601DateTime_validDateWithoutSeconds_returnsTrue() {
+ assertThat(DateTimeFormatValidator.validateISO8601DateTime("2022-01-01T00:00"))
+ .isTrue();
+ }
+
+ @Test
public void testValidateISO8601DateTime_invalidDate_returnsFalse() {
assertThat(DateTimeFormatValidator.validateISO8601DateTime("2022:01:01T00-00-00"))
.isFalse();
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
index d2015d1..48d2840 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/Timer.java
@@ -247,8 +247,8 @@
}
/**
- * Returns the amount of time remaining in milliseconds for the {@link Timer} since it was
- * started, paused or reset.
+ * Returns the amount of time remaining in milliseconds for the {@link Timer} since its state
+ * last changed.
*
* <p>If it is in the {@link #STATUS_STARTED} state, then the current remaining time will be
* different from this value. To get the current remaining time, use
@@ -472,8 +472,8 @@
}
/**
- * Sets the amount of time remaining in milliseconds for the {@link Timer} since it was
- * started, paused or reset.
+ * Sets the amount of time remaining in milliseconds for the {@link Timer} since its
+ * state last changed.
*/
@NonNull
public Builder setRemainingTimeMillisSinceUpdate(long remainingTimeMillisSinceUpdate) {
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/utils/DateTimeFormatValidator.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/utils/DateTimeFormatValidator.java
index c472161..467f10f 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/utils/DateTimeFormatValidator.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/utils/DateTimeFormatValidator.java
@@ -46,7 +46,8 @@
* Returns true if the date string matches yyyy-MM-ddTHH:mm:ss
*/
public static boolean validateISO8601DateTime(@NonNull String dateString) {
- return validateDateFormat("yyyy-MM-dd'T'HH:mm:ss", dateString);
+ return validateDateFormat("yyyy-MM-dd'T'HH:mm", dateString)
+ || validateDateFormat("yyyy-MM-dd'T'HH:mm:ss", dateString);
}
/**
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
index b94be04..b64900d5 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
@@ -84,11 +84,13 @@
Display maxDisplay = null;
int maxDisplaySize = -1;
for (Display display : displays) {
- Point displaySize = new Point();
- display.getRealSize(displaySize);
- if (displaySize.x * displaySize.y > maxDisplaySize) {
- maxDisplaySize = displaySize.x * displaySize.y;
- maxDisplay = display;
+ if (display.getState() != Display.STATE_OFF) {
+ Point displaySize = new Point();
+ display.getRealSize(displaySize);
+ if (displaySize.x * displaySize.y > maxDisplaySize) {
+ maxDisplaySize = displaySize.x * displaySize.y;
+ maxDisplay = display;
+ }
}
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt
index 2815d19..fafca66 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt
@@ -21,6 +21,7 @@
import android.hardware.display.DisplayManager
import android.os.Build
import android.util.Size
+import android.view.Display
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import org.junit.After
@@ -32,16 +33,24 @@
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
import org.robolectric.shadows.ShadowDisplayManager
+import org.robolectric.shadows.ShadowDisplay
@RunWith(RobolectricTestRunner::class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@Suppress("DEPRECATION") // getRealSize
class DisplayInfoManagerTest {
- private fun addDisplay(width: Int, height: Int) {
+
+ private fun addDisplay(width: Int, height: Int, state: Int = Display.STATE_ON) {
val displayStr = String.format("w%ddp-h%ddp", width, height)
- ShadowDisplayManager.addDisplay(displayStr)
+ val displayId = ShadowDisplayManager.addDisplay(displayStr)
+ if (state != Display.STATE_ON) {
+ val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+ .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+ (Shadow.extract(displayManager.getDisplay(displayId)) as ShadowDisplay).setState(state)
+ }
}
@After
@@ -84,6 +93,23 @@
}
@Test
+ fun canReturnMaxSizeDisplay_offState() {
+ // Arrange
+ addDisplay(2000, 3000, Display.STATE_OFF)
+ addDisplay(480, 640)
+ addDisplay(200, 300, Display.STATE_OFF)
+
+ // Act
+ val displayInfoManager = DisplayInfoManager
+ .getInstance(ApplicationProvider.getApplicationContext())
+ val size = Point()
+ displayInfoManager.maxSizeDisplay.getRealSize(size)
+
+ // Assert
+ assertThat(size).isEqualTo(Point(480, 640))
+ }
+
+ @Test
fun canReturnPreviewSize_displaySmallerThan1080P() {
// Arrange
addDisplay(480, 640)
diff --git a/camera/camera-previewview/src/main/java/androidx/camera/previewview/PreviewSurfaceRequest.java b/camera/camera-previewview/src/main/java/androidx/camera/previewview/PreviewSurfaceRequest.java
index dedff4f..2636e46 100644
--- a/camera/camera-previewview/src/main/java/androidx/camera/previewview/PreviewSurfaceRequest.java
+++ b/camera/camera-previewview/src/main/java/androidx/camera/previewview/PreviewSurfaceRequest.java
@@ -75,10 +75,6 @@
@NonNull private TransformationInfo mTransformationInfo;
- @SuppressWarnings("WeakerAccess") /*synthetic accessor */
- @Nullable
- Callback mCallback;
-
@Nullable private Executor mTransformationInfoExecutor;
@Nullable private TransformationInfoListener mTransformationInfoListener;
@@ -195,9 +191,6 @@
// is safe to release the Surface and associated resources.
Futures.propagate(terminationFuture, sessionStatusCompleter);
- if (mCallback != null) {
- mCallback.onSuccess(surface);
- }
}
@Override
@@ -214,9 +207,6 @@
} else {
sessionStatusCompleter.set(null);
}
- if (mCallback != null) {
- mCallback.onFailure(t);
- }
}
}, CameraExecutors.directExecutor());
@@ -266,23 +256,6 @@
}
/**
- * Set surface request callback.
- *
- * @param callback {@link Callback}.
- */
- @SuppressLint("ExecutorRegistration")
- public void setCallback(@NonNull Callback callback) {
- mCallback = callback;
- }
-
- /**
- * Clear surface request callback.
- */
- public void clearCallback() {
- mCallback = null;
- }
-
- /**
* Closes the preview surface to mark it as safe to release.
*/
public void markSurfaceSafeToRelease() {
diff --git a/camera/integration-tests/previewviewtestapp/build.gradle b/camera/integration-tests/previewviewtestapp/build.gradle
new file mode 100644
index 0000000..1ba1215
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/build.gradle
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.application")
+ id("kotlin-android")
+ id("AndroidXComposePlugin")
+}
+
+android {
+ defaultConfig {
+ applicationId "androidx.camera.integration.previewview"
+ minSdkVersion 21
+ multiDexEnabled true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ }
+ }
+}
+
+dependencies {
+ implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
+
+ // Internal library
+ implementation(project(":camera:camera-camera2"))
+ implementation(project(":camera:camera-lifecycle"))
+ implementation(project(":camera:camera-previewview"))
+
+ // Lifecycle and LiveData
+ implementation("androidx.lifecycle:lifecycle-livedata:2.2.0")
+ implementation("androidx.lifecycle:lifecycle-runtime:2.3.1")
+ implementation(project(":lifecycle:lifecycle-common-java8"))
+
+ // Android Support Library
+ implementation("androidx.appcompat:appcompat:1.3.0")
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+
+ implementation(libs.guavaAndroid)
+ implementation(libs.constraintLayout)
+
+ compileOnly(libs.kotlinCompiler)
+
+ // Testing framework
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.testUiautomator)
+ androidTestImplementation(libs.espressoCore)
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime:2.3.1")
+ androidTestImplementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0")
+ androidTestImplementation(libs.truth)
+ debugImplementation(libs.testCore)
+ debugImplementation("androidx.fragment:fragment-testing:1.2.3")
+ // camera-testing added as 'implementation' dependency to include camera-testing activity in APK
+ debugImplementation(project(":camera:camera-testing")) {
+ // Ensure camera-testing does not pull in camera-core project dependency which will
+ // override pinned dependency.
+ exclude(group:"androidx.camera", module:"camera-core")
+ }
+}
diff --git a/camera/integration-tests/previewviewtestapp/src/main/AndroidManifest.xml b/camera/integration-tests/previewviewtestapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f151fcd
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.integration.previewview">
+
+ <uses-permission android:name="android.permission.CAMERA" />
+
+ <uses-feature android:name="android.hardware.camera" />
+ <uses-feature android:name="android.hardware.camera.autofocus" />
+
+ <application
+ android:allowBackup="true"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme">
+
+ <activity
+ android:name=".CameraActivity"
+ android:exported="true"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/camera/integration-tests/previewviewtestapp/src/main/java/androidx/camera/integration/previewview/CameraActivity.java b/camera/integration-tests/previewviewtestapp/src/main/java/androidx/camera/integration/previewview/CameraActivity.java
new file mode 100755
index 0000000..0479435
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/java/androidx/camera/integration/previewview/CameraActivity.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 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.camera.integration.previewview;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * Activity for PreviewView.
+ */
+public class CameraActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_camera);
+ if (null == savedInstanceState) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.container, new CameraViewFinderFragment())
+ .commit();
+ }
+ }
+
+}
diff --git a/camera/integration-tests/previewviewtestapp/src/main/java/androidx/camera/integration/previewview/CameraViewFinderFragment.java b/camera/integration-tests/previewviewtestapp/src/main/java/androidx/camera/integration/previewview/CameraViewFinderFragment.java
new file mode 100755
index 0000000..c4737ad
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/java/androidx/camera/integration/previewview/CameraViewFinderFragment.java
@@ -0,0 +1,1004 @@
+/*
+ * 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.camera.integration.previewview;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.PackageManager;
+import android.graphics.ImageFormat;
+import android.graphics.Point;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.util.Size;
+import android.util.SparseIntArray;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.impl.utils.futures.FutureCallback;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.camera.previewview.CameraViewFinder;
+import androidx.camera.previewview.PreviewSurfaceRequest;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+
+import com.google.common.base.Objects;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Fragment for {@link CameraViewFinder}.
+ */
+public class CameraViewFinderFragment extends Fragment
+ implements View.OnClickListener, ActivityCompat.OnRequestPermissionsResultCallback {
+
+ /**
+ * Conversion from screen rotation to JPEG orientation.
+ */
+ private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
+ private static final int REQUEST_CAMERA_PERMISSION = 1;
+ private static final String FRAGMENT_DIALOG = "dialog";
+
+ static {
+ ORIENTATIONS.append(Surface.ROTATION_0, 90);
+ ORIENTATIONS.append(Surface.ROTATION_90, 0);
+ ORIENTATIONS.append(Surface.ROTATION_180, 270);
+ ORIENTATIONS.append(Surface.ROTATION_270, 180);
+ }
+
+ /**
+ * Tag for the {@link Log}.
+ */
+ private static final String TAG = "Camera2BasicFragment";
+
+ /**
+ * Camera state: Showing camera preview.
+ */
+ private static final int STATE_PREVIEW = 0;
+
+ /**
+ * Camera state: Waiting for the focus to be locked.
+ */
+ private static final int STATE_WAITING_LOCK = 1;
+
+ /**
+ * Camera state: Waiting for the exposure to be precapture state.
+ */
+ private static final int STATE_WAITING_PRECAPTURE = 2;
+
+ /**
+ * Camera state: Waiting for the exposure state to be something other than precapture.
+ */
+ private static final int STATE_WAITING_NON_PRECAPTURE = 3;
+
+ /**
+ * Camera state: Picture was taken.
+ */
+ private static final int STATE_PICTURE_TAKEN = 4;
+
+ /**
+ * Max preview width that is guaranteed by Camera2 API
+ */
+ private static final int MAX_PREVIEW_WIDTH = 1920;
+
+ /**
+ * Max preview height that is guaranteed by Camera2 API
+ */
+ private static final int MAX_PREVIEW_HEIGHT = 1080;
+
+ /**
+ * ID of the current {@link CameraDevice}.
+ */
+ private String mCameraId;
+
+ /**
+ * An instance of {@link CameraManager}.
+ */
+ private CameraManager mCameraManager;
+
+ /**
+ * An {@link CameraViewFinder} for camera preview.
+ */
+ private CameraViewFinder mCameraViewFinder;
+
+ /**
+ * A {@link CameraCaptureSession } for camera preview.
+ */
+ private CameraCaptureSession mCaptureSession;
+
+ /**
+ * A reference to the opened {@link CameraDevice}.
+ */
+ private CameraDevice mCameraDevice;
+
+ /**
+ * The {@link android.util.Size} of camera preview.
+ */
+ private Size mPreviewSize;
+
+ /**
+ * The request for preview surface.
+ */
+ private PreviewSurfaceRequest mPreviewSurfaceRequest;
+
+ /**
+ * {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its state.
+ */
+ private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
+ @Override
+ public void onOpened(@NonNull CameraDevice cameraDevice) {
+ // This method is called when the camera is opened. We start camera preview here.
+ mCameraOpenCloseLock.release();
+ mCameraDevice = cameraDevice;
+ createCameraPreviewSession();
+ }
+
+ @Override
+ public void onDisconnected(@NonNull CameraDevice cameraDevice) {
+ mCameraOpenCloseLock.release();
+ cameraDevice.close();
+ mCameraDevice = null;
+ }
+
+ @Override
+ public void onError(@NonNull CameraDevice cameraDevice, int error) {
+ mCameraOpenCloseLock.release();
+ cameraDevice.close();
+ mCameraDevice = null;
+ Activity activity = getActivity();
+ if (null != activity) {
+ activity.finish();
+ }
+ }
+ };
+
+ /**
+ * An additional thread for running tasks that shouldn't block the UI.
+ */
+ private HandlerThread mBackgroundThread;
+
+ /**
+ * A {@link Handler} for running tasks in the background.
+ */
+ private Handler mBackgroundHandler;
+
+ /**
+ * An {@link ImageReader} that handles still image capture.
+ */
+ private ImageReader mImageReader;
+
+ /**
+ * This is the output file for our picture.
+ */
+ private File mFile;
+
+ /**
+ * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a
+ * still image is ready to be saved.
+ */
+ private final ImageReader.OnImageAvailableListener mOnImageAvailableListener =
+ new ImageReader.OnImageAvailableListener() {
+ @Override
+ public void onImageAvailable(ImageReader reader) {
+ mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), mFile));
+ }
+ };
+
+ /**
+ * {@link CaptureRequest.Builder} for the camera preview
+ */
+ private CaptureRequest.Builder mPreviewRequestBuilder;
+
+ /**
+ * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder}
+ */
+ private CaptureRequest mPreviewRequest;
+
+ /**
+ * The current state of camera state for taking pictures.
+ *
+ * @see #mCaptureCallback
+ */
+ private int mState = STATE_PREVIEW;
+
+ /**
+ * A {@link Semaphore} to prevent the app from exiting before closing the camera.
+ */
+ private Semaphore mCameraOpenCloseLock = new Semaphore(1);
+
+ /**
+ * Whether the current camera device supports Flash or not.
+ */
+ private boolean mFlashSupported;
+
+ /**
+ * Orientation of the camera sensor
+ */
+ private int mSensorOrientation;
+
+ /**
+ * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture.
+ */
+ private CameraCaptureSession.CaptureCallback mCaptureCallback =
+ new CameraCaptureSession.CaptureCallback() {
+
+ private void process(CaptureResult result) {
+ switch (mState) {
+ case STATE_PREVIEW: {
+ // We have nothing to do when the camera preview is working normally.
+ break;
+ }
+ case STATE_WAITING_LOCK: {
+ Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
+ if (afState == null) {
+ captureStillPicture();
+ } else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState
+ || CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState) {
+ // CONTROL_AE_STATE can be null on some devices
+ Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
+ if (aeState == null
+ || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
+ mState = STATE_PICTURE_TAKEN;
+ captureStillPicture();
+ } else {
+ runPrecaptureSequence();
+ }
+ }
+ break;
+ }
+ case STATE_WAITING_PRECAPTURE: {
+ // CONTROL_AE_STATE can be null on some devices
+ Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
+ if (aeState == null
+ || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE
+ || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) {
+ mState = STATE_WAITING_NON_PRECAPTURE;
+ }
+ break;
+ }
+ case STATE_WAITING_NON_PRECAPTURE: {
+ // CONTROL_AE_STATE can be null on some devices
+ Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
+ if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
+ mState = STATE_PICTURE_TAKEN;
+ captureStillPicture();
+ }
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onCaptureProgressed(@NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull CaptureResult partialResult) {
+ process(partialResult);
+ }
+
+ @Override
+ public void onCaptureCompleted(@NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull TotalCaptureResult result) {
+ process(result);
+ }
+
+ };
+
+ /**
+ * Shows a {@link Toast} on the UI thread.
+ *
+ * @param text The message to show
+ */
+ private void showToast(final String text) {
+ final Activity activity = getActivity();
+ if (activity != null) {
+ activity.runOnUiThread(() -> Toast.makeText(activity, text, Toast.LENGTH_SHORT).show());
+ }
+ }
+
+ /**
+ * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that
+ * is at least as large as the respective texture view size, and that is at most as large as the
+ * respective max size, and whose aspect ratio matches with the specified value. If such size
+ * doesn't exist, choose the largest one that is at most as large as the respective max size,
+ * and whose aspect ratio matches with the specified value.
+ *
+ * @param choices The list of sizes that the camera supports for the intended output
+ * class
+ * @param textureViewWidth The width of the texture view relative to sensor coordinate
+ * @param textureViewHeight The height of the texture view relative to sensor coordinate
+ * @param maxWidth The maximum width that can be chosen
+ * @param maxHeight The maximum height that can be chosen
+ * @param aspectRatio The aspect ratio
+ * @return The optimal {@code Size}, or an arbitrary one if none were big enough
+ */
+ private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
+ int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
+
+ // Collect the supported resolutions that are at least as big as the preview Surface
+ List<Size> bigEnough = new ArrayList<>();
+ // Collect the supported resolutions that are smaller than the preview Surface
+ List<Size> notBigEnough = new ArrayList<>();
+ int w = aspectRatio.getWidth();
+ int h = aspectRatio.getHeight();
+ for (Size option : choices) {
+ if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight
+ && option.getHeight() == option.getWidth() * h / w) {
+ if (option.getWidth() >= textureViewWidth
+ && option.getHeight() >= textureViewHeight) {
+ bigEnough.add(option);
+ } else {
+ notBigEnough.add(option);
+ }
+ }
+ }
+
+ // Pick the smallest of those big enough. If there is no one big enough, pick the
+ // largest of those not big enough.
+ if (bigEnough.size() > 0) {
+ return Collections.min(bigEnough, new CompareSizesByArea());
+ } else if (notBigEnough.size() > 0) {
+ return Collections.max(notBigEnough, new CompareSizesByArea());
+ } else {
+ Log.e(TAG, "Couldn't find any suitable preview size");
+ return choices[0];
+ }
+ }
+
+ /**
+ * Returns {@link CameraViewFinderFragment} instance.
+ * @return {@link CameraViewFinderFragment}.
+ */
+ @NonNull
+ public static CameraViewFinderFragment newInstance() {
+ return new CameraViewFinderFragment();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_camera_view_finder, container, false);
+ }
+
+ @SuppressLint("ClassVerificationFailure")
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ view.findViewById(R.id.picture).setOnClickListener(this);
+ view.findViewById(R.id.toggle).setOnClickListener(this);
+ mCameraViewFinder = view.findViewById(R.id.view_finder);
+
+ if (Build.VERSION.SDK_INT >= 23) {
+ mCameraManager = getActivity().getSystemService(CameraManager.class);
+ } else {
+ mCameraManager = (CameraManager) getActivity().getSystemService(Context.CAMERA_SERVICE);
+ }
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mFile = new File(getActivity().getExternalFilesDir(null), "pic.jpg");
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ startBackgroundThread();
+
+ // When the screen is turned off and turned back on, the SurfaceTexture is already
+ // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open
+ // a camera and start preview from here (otherwise, we wait until the surface is ready in
+ // the SurfaceTextureListener).
+ mCameraViewFinder.post(() -> {
+ openCamera(mCameraViewFinder.getWidth(), mCameraViewFinder.getHeight(), false);
+ });
+ }
+
+ @Override
+ public void onPause() {
+ closeCamera();
+ stopBackgroundThread();
+ super.onPause();
+ }
+
+ private void requestCameraPermission() {
+ if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
+ new ConfirmationDialog().show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ } else {
+ requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ if (requestCode == REQUEST_CAMERA_PERMISSION) {
+ if (grantResults.length != 1 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+ ErrorDialog.newInstance(getString(R.string.request_permission))
+ .show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ }
+ } else {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ /**
+ * Sets up member variables related to camera.
+ *
+ * @param width The width of available size for camera preview
+ * @param height The height of available size for camera preview
+ * @param toggleCamera Whether to toggle front and back camera or not.
+ */
+ @SuppressWarnings("SuspiciousNameCombination")
+ private void setUpCameraOutputs(int width, int height, boolean toggleCamera) {
+ try {
+ for (String cameraId : mCameraManager.getCameraIdList()) {
+ CameraCharacteristics characteristics =
+ mCameraManager.getCameraCharacteristics(cameraId);
+ Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
+
+ // Toggle the front and back camera
+ if (toggleCamera && mCameraId != null) {
+ Integer currentFacing =
+ mCameraManager.getCameraCharacteristics(mCameraId)
+ .get(CameraCharacteristics.LENS_FACING);
+
+ if (Objects.equal(currentFacing, facing)) {
+ continue;
+ }
+ }
+
+ StreamConfigurationMap map = characteristics.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+ if (map == null) {
+ continue;
+ }
+
+ // For still image captures, we use the largest available size.
+ Size largest = Collections.max(
+ Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
+ new CompareSizesByArea());
+ mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
+ ImageFormat.JPEG, /*maxImages*/2);
+ mImageReader.setOnImageAvailableListener(
+ mOnImageAvailableListener, mBackgroundHandler);
+
+ // Find out if we need to swap dimension to get the preview size relative to sensor
+ // coordinate.
+
+ // TODO: CameraViewFinder gets its own display instead of being provided by the
+ // app. It has 2 benefits:
+ // 1) one less line of boilerplate code for developers
+ // 2) it will always use the correct display
+ Display display = mCameraViewFinder.getDisplay();
+ int displayRotation = display.getRotation();
+ mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+ boolean swappedDimensions = false;
+ switch (displayRotation) {
+ case Surface.ROTATION_0:
+ case Surface.ROTATION_180:
+ if (mSensorOrientation == 90 || mSensorOrientation == 270) {
+ swappedDimensions = true;
+ }
+ break;
+ case Surface.ROTATION_90:
+ case Surface.ROTATION_270:
+ if (mSensorOrientation == 0 || mSensorOrientation == 180) {
+ swappedDimensions = true;
+ }
+ break;
+ default:
+ Log.e(TAG, "Display rotation is invalid: " + displayRotation);
+ }
+
+ Point displaySize = new Point();
+ display.getSize(displaySize);
+ int rotatedPreviewWidth = width;
+ int rotatedPreviewHeight = height;
+ int maxPreviewWidth = displaySize.x;
+ int maxPreviewHeight = displaySize.y;
+
+ if (swappedDimensions) {
+ rotatedPreviewWidth = height;
+ rotatedPreviewHeight = width;
+ maxPreviewWidth = displaySize.y;
+ maxPreviewHeight = displaySize.x;
+ }
+
+ if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
+ maxPreviewWidth = MAX_PREVIEW_WIDTH;
+ }
+
+ if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
+ maxPreviewHeight = MAX_PREVIEW_HEIGHT;
+ }
+
+ mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
+ rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth,
+ maxPreviewHeight, largest);
+
+ // Check if the flash is supported.
+ Boolean available = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+ mFlashSupported = available == null ? false : available;
+ mCameraId = cameraId;
+ mPreviewSurfaceRequest = new PreviewSurfaceRequest(
+ mPreviewSize,
+ display,
+ characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
+ == CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+ facing == CameraCharacteristics.LENS_FACING_FRONT,
+ mSensorOrientation);
+ return;
+ }
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "setUpCameraOutputs CameraAccessException message = " + e.getMessage());
+ }
+ }
+
+ /**
+ * Opens the camera specified by Camera CameraId.
+ */
+ private void openCamera(int width, int height, boolean toggleCamera) {
+ if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
+ != PackageManager.PERMISSION_GRANTED) {
+ requestCameraPermission();
+ return;
+ }
+ setUpCameraOutputs(width, height, toggleCamera);
+ try {
+ if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
+ throw new RuntimeException("Time out waiting to lock camera opening.");
+ }
+ mCameraManager.openCamera(mCameraId, mStateCallback, null);
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "openCamera CameraAccessException message = " + e.getMessage());
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
+ }
+ }
+
+ /**
+ * Closes the current {@link CameraDevice}.
+ */
+ private void closeCamera() {
+ try {
+ mCameraOpenCloseLock.acquire();
+ if (null != mCaptureSession) {
+ mCaptureSession.close();
+ mCaptureSession = null;
+ }
+ if (null != mCameraDevice) {
+ mCameraDevice.close();
+ mCameraDevice = null;
+ }
+ if (null != mImageReader) {
+ mImageReader.close();
+ mImageReader = null;
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while trying to lock camera closing.", e);
+ } finally {
+ mCameraOpenCloseLock.release();
+ }
+ }
+
+ /**
+ * Starts a background thread and its {@link Handler}.
+ */
+ private void startBackgroundThread() {
+ mBackgroundThread = new HandlerThread("CameraBackground");
+ mBackgroundThread.start();
+ mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
+ }
+
+ /**
+ * Stops the background thread and its {@link Handler}.
+ */
+ private void stopBackgroundThread() {
+ mBackgroundThread.quitSafely();
+ try {
+ mBackgroundThread.join();
+ mBackgroundThread = null;
+ mBackgroundHandler = null;
+ } catch (InterruptedException e) {
+ Log.e(TAG, "stopBackgroundThread InterruptedException message = "
+ + e.getMessage());
+ }
+ }
+
+ /**
+ * Creates a new {@link CameraCaptureSession} for camera preview.
+ */
+ private void createCameraPreviewSession() {
+ ListenableFuture<Surface> surfaceListenableFuture =
+ mCameraViewFinder.requestSurfaceAsync(mPreviewSurfaceRequest);
+
+ Futures.addCallback(surfaceListenableFuture, new FutureCallback<Surface>() {
+ @Override
+ public void onSuccess(@Nullable Surface surface) {
+ Log.d(TAG,
+ "request onSurfaceAvailable surface = " + surface);
+ if (surface != null) {
+ createCaptureSession(surface);
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ Log.e(TAG, "request onSurfaceClosed");
+ }
+ }, ContextCompat.getMainExecutor(getContext()));
+ }
+
+ private void createCaptureSession(@NonNull Surface surface) {
+ try {
+ mPreviewRequestBuilder =
+ mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+ mPreviewRequestBuilder.addTarget(surface);
+
+ mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
+ new CameraCaptureSession.StateCallback() {
+
+ @Override
+ public void onConfigured(
+ @NonNull CameraCaptureSession cameraCaptureSession) {
+ // The camera is already closed
+ if (null == mCameraDevice) {
+ return;
+ }
+
+ // When the session is ready, we start displaying the preview.
+ mCaptureSession = cameraCaptureSession;
+ try {
+ // Auto focus should be continuous for camera preview.
+ mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+ // Flash is automatically enabled when necessary.
+ setAutoFlash(mPreviewRequestBuilder);
+
+ // Finally, we start displaying the camera preview.
+ mPreviewRequest = mPreviewRequestBuilder.build();
+ mCaptureSession.setRepeatingRequest(mPreviewRequest,
+ mCaptureCallback, null);
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "onConfigured CameraAccessException message = "
+ + e.getMessage());
+ }
+ }
+
+ @Override
+ public void onConfigureFailed(
+ @NonNull CameraCaptureSession cameraCaptureSession) {
+ showToast("Failed");
+ }
+ }, null
+ );
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "createCaptureSession CameraAccessException message = " + e.getMessage());
+ }
+ }
+
+ /**
+ * Initiate a still image capture.
+ */
+ private void takePicture() {
+ lockFocus();
+ }
+
+ /**
+ * Toggle the front and back camera
+ */
+ private void toggleCamera() {
+ closeCamera();
+ openCamera(mCameraViewFinder.getWidth(), mCameraViewFinder.getHeight(), true);
+ }
+
+ /**
+ * Lock the focus as the first step for a still image capture.
+ */
+ private void lockFocus() {
+ try {
+ // This is how to tell the camera to lock focus.
+ mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
+ CameraMetadata.CONTROL_AF_TRIGGER_START);
+ // Tell #mCaptureCallback to wait for the lock.
+ mState = STATE_WAITING_LOCK;
+ mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
+ null);
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "lockFocus CameraAccessException message = " + e.getMessage());
+ }
+ }
+
+ /**
+ * Run the precapture sequence for capturing a still image. This method should be called when
+ * we get a response in {@link #mCaptureCallback} from {@link #lockFocus()}.
+ */
+ private void runPrecaptureSequence() {
+ try {
+ // This is how to tell the camera to trigger.
+ mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+ CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
+ // Tell #mCaptureCallback to wait for the precapture sequence to be set.
+ mState = STATE_WAITING_PRECAPTURE;
+ mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
+ null);
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "runPrecaptureSequence CameraAccessException message = " + e.getMessage());
+ }
+ }
+
+ /**
+ * Capture a still picture. This method should be called when we get a response in
+ * {@link #mCaptureCallback} from both {@link #lockFocus()}.
+ */
+ private void captureStillPicture() {
+ try {
+ final Activity activity = getActivity();
+ if (null == activity || null == mCameraDevice) {
+ return;
+ }
+ // This is the CaptureRequest.Builder that we use to take a picture.
+ final CaptureRequest.Builder captureBuilder =
+ mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
+ captureBuilder.addTarget(mImageReader.getSurface());
+
+ // Use the same AE and AF modes as the preview.
+ captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+ setAutoFlash(captureBuilder);
+
+ // Orientation
+ int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
+ captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
+
+ CameraCaptureSession.CaptureCallback captureCallback =
+ new CameraCaptureSession.CaptureCallback() {
+
+ @Override
+ public void onCaptureCompleted(@NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull TotalCaptureResult result) {
+ showToast("Saved: " + mFile);
+ Log.d(TAG, mFile.toString());
+ unlockFocus();
+ }
+ };
+
+ mCaptureSession.stopRepeating();
+ mCaptureSession.abortCaptures();
+ mCaptureSession.capture(captureBuilder.build(), captureCallback, null);
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "captureStillPicture CameraAccessException message = " + e.getMessage());
+ }
+ }
+
+ /**
+ * Retrieves the JPEG orientation from the specified screen rotation.
+ *
+ * @param rotation The screen rotation.
+ * @return The JPEG orientation (one of 0, 90, 270, and 360)
+ */
+ private int getOrientation(int rotation) {
+ // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X)
+ // We have to take that into account and rotate JPEG properly.
+ // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS.
+ // For devices with orientation of 270, we need to rotate the JPEG 180 degrees.
+ return (ORIENTATIONS.get(rotation) + mSensorOrientation + 270) % 360;
+ }
+
+ /**
+ * Unlock the focus. This method should be called when still image capture sequence is
+ * finished.
+ */
+ private void unlockFocus() {
+ try {
+ // Reset the auto-focus trigger
+ mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
+ CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
+ setAutoFlash(mPreviewRequestBuilder);
+ mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
+ null);
+ // After this, the camera will go back to the normal state of preview.
+ mState = STATE_PREVIEW;
+ mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback,
+ null);
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "unlockFocus CameraAccessException message = " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.picture: {
+ takePicture();
+ break;
+ }
+ case R.id.toggle: {
+ toggleCamera();
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ private void setAutoFlash(CaptureRequest.Builder requestBuilder) {
+ if (mFlashSupported) {
+ requestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
+ }
+ }
+
+ /**
+ * Saves a JPEG {@link Image} into the specified {@link File}.
+ */
+ private static class ImageSaver implements Runnable {
+
+ /**
+ * The JPEG image
+ */
+ private final Image mImage;
+ /**
+ * The file we save the image into.
+ */
+ private final File mFile;
+
+ ImageSaver(Image image, File file) {
+ mImage = image;
+ mFile = file;
+ }
+
+ @Override
+ public void run() {
+ ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
+ byte[] bytes = new byte[buffer.remaining()];
+ buffer.get(bytes);
+
+ try (Image imageToClose = mImage;
+ FileOutputStream output = new FileOutputStream(mFile)) {
+ output.write(bytes);
+ } catch (IOException e) {
+ Log.e(TAG, "ImageSaver CameraAccessException message = " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Compares two {@code Size}s based on their areas.
+ */
+ static class CompareSizesByArea implements Comparator<Size> {
+
+ @Override
+ public int compare(Size lhs, Size rhs) {
+ // We cast here to ensure the multiplications won't overflow
+ return Long.signum((long) lhs.getWidth() * lhs.getHeight()
+ - (long) rhs.getWidth() * rhs.getHeight());
+ }
+
+ }
+
+ /**
+ * Shows an error message dialog.
+ */
+ public static class ErrorDialog extends DialogFragment {
+
+ private static final String ARG_MESSAGE = "message";
+
+ /**
+ * Returns {@link ErrorDialog} instance.
+ * @param message text message to show.
+ * @return {@link ErrorDialog}.
+ */
+ @NonNull
+ public static ErrorDialog newInstance(@NonNull String message) {
+ ErrorDialog dialog = new ErrorDialog();
+ Bundle args = new Bundle();
+ args.putString(ARG_MESSAGE, message);
+ dialog.setArguments(args);
+ return dialog;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+ return new AlertDialog.Builder(activity)
+ .setMessage(getArguments().getString(ARG_MESSAGE))
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ activity.finish();
+ }
+ })
+ .create();
+ }
+ }
+
+ /**
+ * Shows OK/Cancel confirmation dialog about camera permission.
+ */
+ public static class ConfirmationDialog extends DialogFragment {
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ final Fragment parent = getParentFragment();
+ return new AlertDialog.Builder(getActivity())
+ .setMessage(R.string.request_permission)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ parent.requestPermissions(new String[]{Manifest.permission.CAMERA},
+ REQUEST_CAMERA_PERMISSION);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Activity activity = parent.getActivity();
+ if (activity != null) {
+ activity.finish();
+ }
+ }
+ })
+ .create();
+ }
+ }
+}
diff --git a/camera/integration-tests/previewviewtestapp/src/main/res/layout/activity_camera.xml b/camera/integration-tests/previewviewtestapp/src/main/res/layout/activity_camera.xml
new file mode 100644
index 0000000..8005442
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/res/layout/activity_camera.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#000"/>
diff --git a/camera/integration-tests/previewviewtestapp/src/main/res/layout/fragment_camera_view_finder.xml b/camera/integration-tests/previewviewtestapp/src/main/res/layout/fragment_camera_view_finder.xml
new file mode 100644
index 0000000..33a2e58
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/res/layout/fragment_camera_view_finder.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <androidx.camera.previewview.CameraViewFinder
+ android:id="@+id/view_finder"
+ app:scaleType="fitCenter"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <RelativeLayout
+ android:id="@+id/control"
+ android:layout_width="match_parent"
+ android:layout_height="112dp"
+ android:gravity="center|bottom"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true">
+
+ <Button
+ android:id="@+id/picture"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="20dp"
+ android:padding="20dp"
+ android:layout_gravity="center"
+ android:text="@string/picture" />
+
+ <Button
+ android:id="@+id/toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_toRightOf="@+id/picture"
+ android:layout_margin="20dp"
+ android:padding="20dp"
+ android:text="@string/toggle" />
+
+ </RelativeLayout>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/camera/integration-tests/previewviewtestapp/src/main/res/values/donottranslate-strings.xml b/camera/integration-tests/previewviewtestapp/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..a579d51
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <string name="app_name">CameraViewFinderTest</string>
+ <string name="intro_message">This sample demonstrates the basic use of CameraViewFinder API.</string>
+ <string name="picture">Picture</string>
+ <string name="toggle">Toggle</string>
+ <string name="description_info">Info</string>
+ <string name="request_permission">This sample needs camera permission.</string>
+
+</resources>
diff --git a/camera/integration-tests/previewviewtestapp/src/main/res/values/styles.xml b/camera/integration-tests/previewviewtestapp/src/main/res/values/styles.xml
new file mode 100644
index 0000000..b6303c4
--- /dev/null
+++ b/camera/integration-tests/previewviewtestapp/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2017 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.
+-->
+<resources>
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar" />
+</resources>
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
index e15984a..a9298ff 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
@@ -108,11 +108,15 @@
android:exported="true"
android:launchMode="singleTask"
android:label="Showcase">
-
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
+ <intent-filter>
+ <action android:name="androidx.car.app.action.NAVIGATE" />
+ <category android:name="android.intent.category.DEFAULT"/>
+ <data android:scheme="geo" />
+ </intent-filter>
<meta-data android:name="distractionOptimized" android:value="true"/>
</activity>
@@ -122,17 +126,10 @@
android:exported="true"
android:launchMode="singleTask"
android:label="Showcase - Debug">
-
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
-
- <intent-filter>
- <action android:name="androidx.car.app.action.NAVIGATE" />
- <category android:name="android.intent.category.DEFAULT"/>
- <data android:scheme="geo" />
- </intent-filter>
</activity>
</application>
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
index 54b62dca..13e311ad 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
@@ -115,11 +115,15 @@
android:exported="true"
android:launchMode="singleTask"
android:label="Showcase">
-
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
+ <intent-filter>
+ <action android:name="androidx.car.app.action.NAVIGATE" />
+ <category android:name="android.intent.category.DEFAULT"/>
+ <data android:scheme="geo" />
+ </intent-filter>
<meta-data android:name="distractionOptimized" android:value="true"/>
</activity>
@@ -129,16 +133,10 @@
android:exported="true"
android:launchMode="singleTask"
android:label="Showcase - Debug">
-
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
- <intent-filter>
- <action android:name="androidx.car.app.action.NAVIGATE" />
- <category android:name="android.intent.category.DEFAULT"/>
- <data android:scheme="geo" />
- </intent-filter>
</activity>
</application>
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 90aafa9..04cf3b3 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -482,7 +482,7 @@
}
public final class LazyGridKt {
- method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.GridCells cells, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyGridScope,kotlin.Unit> content);
+ method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyVerticalGrid(androidx.compose.foundation.lazy.GridCells cells, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyGridState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyGridScope,kotlin.Unit> content);
method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void items(androidx.compose.foundation.lazy.LazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyGridItemSpanScope,? super T,androidx.compose.foundation.lazy.GridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyGridItemScope,? super T,kotlin.Unit> itemContent);
method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void items(androidx.compose.foundation.lazy.LazyGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, optional kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyGridItemSpanScope,? super T,androidx.compose.foundation.lazy.GridItemSpan>? span, optional kotlin.jvm.functions.Function1<? super T,?> contentType, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyGridItemScope,? super T,kotlin.Unit> itemContent);
method @androidx.compose.foundation.ExperimentalFoundationApi public static inline <T> void itemsIndexed(androidx.compose.foundation.lazy.LazyGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyGridItemSpanScope,? super java.lang.Integer,? super T,androidx.compose.foundation.lazy.GridItemSpan>? span, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?> contentType, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyGridItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt
index 446fcf9..0873c9f 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyArrangementsTest.kt
@@ -442,21 +442,22 @@
// with reverseLayout == true
- // @Test
- // fun vertical_defaultArrangementIsBottomWithReverseLayout() {
- // rule.setContent {
- // LazyColumn(
- // reverseLayout = true,
- // modifier = Modifier.requiredSize(containerSize)
- // ) {
- // items(2) {
- // Item(it)
- // }
- // }
- // }
+ @Test
+ fun vertical_defaultArrangementIsBottomWithReverseLayout() {
+ rule.setContent {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ reverseLayout = true,
+ modifier = Modifier.size(containerSize)
+ ) {
+ items(2) {
+ Item(it)
+ }
+ }
+ }
- // assertArrangementForTwoItems(Arrangement.Bottom, reverseLayout = true)
- // }
+ assertArrangementForTwoItems(Arrangement.Bottom, reverseLayout = true)
+ }
// @Test
// fun row_defaultArrangementIsEndWithReverseLayout() {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt
index a938ed8..27d78ef 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridLayoutInfoTest.kt
@@ -47,18 +47,20 @@
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
@MediumTest
@OptIn(ExperimentalFoundationApi::class)
-// @RunWith(Parameterized::class)
+@RunWith(Parameterized::class)
class LazyGridLayoutInfoTest(
- // private val reverseLayout: Boolean = false
+ private val reverseLayout: Boolean
) {
- // companion object {
- // @JvmStatic
- // @Parameterized.Parameters(name = "reverseLayout={0}")
- // fun initParameters(): Array<Any> = arrayOf(false, true)
- // }
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "reverseLayout={0}")
+ fun initParameters(): Array<Any> = arrayOf(false, true)
+ }
@get:Rule
val rule = createComposeRule()
@@ -82,7 +84,7 @@
rule.setContent {
LazyVerticalGrid(
state = rememberLazyGridState().also { state = it },
- // reverseLayout = reverseLayout,
+ reverseLayout = reverseLayout,
modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
cells = GridCells.Fixed(2)
) {
@@ -103,7 +105,7 @@
rule.setContent {
LazyVerticalGrid(
state = rememberLazyGridState().also { state = it },
- // reverseLayout = reverseLayout,
+ reverseLayout = reverseLayout,
modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
cells = GridCells.Fixed(2)
) {
@@ -128,7 +130,7 @@
rule.setContent {
LazyVerticalGrid(
state = rememberLazyGridState().also { state = it },
- // reverseLayout = reverseLayout,
+ reverseLayout = reverseLayout,
verticalArrangement = Arrangement.spacedBy(itemSizeDp),
modifier = Modifier.width(itemSizeDp).height(itemSizeDp * 3.5f),
cells = GridCells.Fixed(1)
@@ -155,7 +157,7 @@
rule.setContent {
LazyVerticalGrid(
state = rememberLazyGridState().also { state = it },
- // reverseLayout = reverseLayout,
+ reverseLayout = reverseLayout,
modifier = Modifier.size(itemSizeDp * 2f, itemSizeDp * 3.5f),
cells = GridCells.Fixed(2)
) {
@@ -193,7 +195,7 @@
rule.setContent {
LazyVerticalGrid(
modifier = Modifier.width(itemSizeDp),
- // reverseLayout = reverseLayout,
+ reverseLayout = reverseLayout,
state = rememberLazyGridState().also { state = it },
cells = GridCells.Fixed(1)
) {
@@ -231,7 +233,7 @@
lateinit var state: LazyGridState
rule.setContent {
LazyVerticalGrid(
- // reverseLayout = reverseLayout,
+ reverseLayout = reverseLayout,
state = rememberLazyGridState().also { state = it },
cells = GridCells.Fixed(2)
) {
@@ -259,7 +261,7 @@
rule.setContent {
LazyVerticalGrid(
modifier = Modifier.height(sizeDp).width(sizeDp * 2),
- // reverseLayout = reverseLayout,
+ reverseLayout = reverseLayout,
state = rememberLazyGridState().also { state = it },
cells = GridCells.Fixed(2)
) {
@@ -283,10 +285,10 @@
val endPaddingPx = 15
val sizeDp = with(rule.density) { sizePx.toDp() }
val topPaddingDp = with(rule.density) {
- if (!false/*reverseLayout*/) startPaddingPx.toDp() else endPaddingPx.toDp()
+ if (!reverseLayout) startPaddingPx.toDp() else endPaddingPx.toDp()
}
val bottomPaddingDp = with(rule.density) {
- if (!false/*reverseLayout*/) endPaddingPx.toDp() else startPaddingPx.toDp()
+ if (!reverseLayout) endPaddingPx.toDp() else startPaddingPx.toDp()
}
lateinit var state: LazyGridState
rule.setContent {
@@ -298,7 +300,7 @@
start = 2.dp,
end = 2.dp
),
- // reverseLayout = reverseLayout,
+ reverseLayout = reverseLayout,
state = rememberLazyGridState().also { state = it },
cells = GridCells.Fixed(2)
) {
@@ -311,8 +313,7 @@
rule.runOnIdle {
assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
- // TODO(b/211753558) currently failing because we need to port aosp/1903956 to grids
- // assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx * 2, sizePx))
+ assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx * 2, sizePx))
}
}
@@ -342,7 +343,7 @@
rule.setContent {
LazyVerticalGrid(
state = rememberLazyGridState().also { state = it },
- // reverseLayout = reverseLayout,
+ reverseLayout = reverseLayout,
modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
cells = GridCells.Fixed(2)
) {
@@ -353,7 +354,7 @@
}
rule.runOnIdle {
- assertThat(state.layoutInfo.reverseLayout).isEqualTo(false/*reverseLayout*/)
+ assertThat(state.layoutInfo.reverseLayout).isEqualTo(reverseLayout)
}
}
@@ -363,7 +364,7 @@
rule.setContent {
LazyVerticalGrid(
state = rememberLazyGridState().also { state = it },
- // reverseLayout = reverseLayout,
+ reverseLayout = reverseLayout,
modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
cells = GridCells.Fixed(2)
) {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
index aa97dda..e7d6655 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPrefetcherTest.kt
@@ -209,37 +209,45 @@
.assertDoesNotExist()
}
- // @Test
- // fun prefetchingForwardAndBackwardReverseLayout() {
- // composeList(firstItem = 1, reverseLayout = true)
+ @Test
+ fun prefetchingForwardAndBackwardReverseLayout() {
+ composeList(firstItem = 2, reverseLayout = true)
- // rule.runOnIdle {
- // runBlocking {
- // state.scrollBy(5f)
- // }
- // }
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(5f)
+ }
+ }
- // waitForPrefetch(3)
+ waitForPrefetch(6)
- // rule.onNodeWithTag("3")
- // .assertExists()
- // rule.onNodeWithTag("0")
- // .assertDoesNotExist()
+ rule.onNodeWithTag("6")
+ .assertExists()
+ rule.onNodeWithTag("7")
+ .assertExists()
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
- // rule.runOnIdle {
- // runBlocking {
- // state.scrollBy(-2f)
- // state.scrollBy(-1f)
- // }
- // }
+ rule.runOnIdle {
+ runBlocking {
+ state.scrollBy(-2f)
+ state.scrollBy(-1f)
+ }
+ }
- // waitForPrefetch(0)
+ waitForPrefetch(0)
- // rule.onNodeWithTag("0")
- // .assertExists()
- // rule.onNodeWithTag("3")
- // .assertDoesNotExist()
- // }
+ rule.onNodeWithTag("0")
+ .assertExists()
+ rule.onNodeWithTag("1")
+ .assertExists()
+ rule.onNodeWithTag("6")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("7")
+ .assertDoesNotExist()
+ }
@Test
fun prefetchingForwardAndBackwardWithContentPadding() {
@@ -347,7 +355,7 @@
private fun composeList(
firstItem: Int = 0,
itemOffset: Int = 0,
- // reverseLayout: Boolean = false,
+ reverseLayout: Boolean = false,
contentPadding: PaddingValues = PaddingValues(0.dp)
) {
rule.setContent {
@@ -359,7 +367,7 @@
GridCells.Fixed(2),
Modifier.height(itemsSizeDp * 1.5f),
state,
- // reverseLayout = reverseLayout,
+ reverseLayout = reverseLayout,
contentPadding = contentPadding
) {
items(100) {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
index 56ca84da..6e61140 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
@@ -31,10 +31,12 @@
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.LazyGridState
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration
import androidx.compose.foundation.lazy.rememberLazyGridState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
@@ -376,41 +378,384 @@
.assertHeightIsEqualTo(12.dp)
}
- // @Test
- // fun verticalGrid_contentPaddingAndReverseLayout() {
- // val topPadding = itemSize * 2
- // val bottomPadding = itemSize / 2
- // val listSize = itemSize * 3
- // lateinit var state: LazyGridState
- // rule.setContentWithTestViewConfiguration {
- // LazyVerticalGrid(
- // reverseLayout = true,
- // state = rememberLazyGridState().also { state = it },
- // modifier = Modifier.requiredSize(listSize),
- // contentPadding = PaddingValues(top = topPadding, bottom = bottomPadding),
- // ) {
- // items(3) { index ->
- // Box(Modifier.requiredSize(itemSize).testTag("$index"))
- // }
- // }
- // }
+ @Test
+ fun verticalGrid_contentPaddingAndReverseLayout() {
+ val topPadding = itemSize * 2
+ val bottomPadding = itemSize / 2
+ val listSize = itemSize * 3
+ lateinit var state: LazyGridState
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ reverseLayout = true,
+ state = rememberLazyGridState().also { state = it },
+ modifier = Modifier.size(listSize),
+ contentPadding = PaddingValues(top = topPadding, bottom = bottomPadding),
+ ) {
+ items(3) { index ->
+ Box(Modifier.size(itemSize).testTag("$index"))
+ }
+ }
+ }
- // rule.onNodeWithTag("0")
- // .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize)
- // rule.onNodeWithTag("1")
- // .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize * 2)
- // // Partially visible.
- // rule.onNodeWithTag("2")
- // .assertTopPositionInRootIsEqualTo(-itemSize / 2)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(listSize - bottomPadding - itemSize * 2)
+ // Partially visible.
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(-itemSize / 2)
- // // Scroll to the top.
- // state.scrollBy(itemSize * 2.5f)
+ // Scroll to the top.
+ state.scrollBy(itemSize * 2.5f)
- // rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(topPadding)
- // // Shouldn't be visible
- // rule.onNodeWithTag("1").assertIsNotDisplayed()
- // rule.onNodeWithTag("0").assertIsNotDisplayed()
- // }
+ rule.onNodeWithTag("2").assertTopPositionInRootIsEqualTo(topPadding)
+ // Shouldn't be visible
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
+ rule.onNodeWithTag("0").assertIsNotDisplayed()
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_initialState() {
+ lateinit var state: LazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(0, 0.dp)
+ state.assertVisibleItems(0 to 0.dp)
+ state.assertLayoutInfoOffsetRange(-itemSize, itemSize * 0.5f)
+ }
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_scrollByPadding() {
+ lateinit var state: LazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(1, 0.dp)
+ state.assertVisibleItems(0 to -itemSize, 1 to 0.dp)
+ }
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_scrollToLastItem() {
+ lateinit var state: LazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollTo(3)
+
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, 0.dp)
+ state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
+ }
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_scrollToLastItemByDelta() {
+ lateinit var state: LazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+
+ rule.onNodeWithTag("1")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, 0.dp)
+ state.assertVisibleItems(2 to -itemSize, 3 to 0.dp)
+ }
+ }
+
+ @Test
+ fun totalPaddingLargerParentSize_scrollTillTheEnd() {
+ // the whole end content padding is displayed
+ lateinit var state: LazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 4.5f)
+
+ rule.onNodeWithTag("2")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(-itemSize * 0.5f)
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, itemSize * 1.5f)
+ state.assertVisibleItems(3 to -itemSize * 1.5f)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_initialState() {
+ lateinit var state: LazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(0, 0.dp)
+ state.assertVisibleItems(0 to 0.dp)
+ state.assertLayoutInfoOffsetRange(-itemSize * 2, -itemSize * 0.5f)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_scrollByPadding() {
+ lateinit var state: LazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 2)
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("2")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(2, 0.dp)
+ state.assertVisibleItems(0 to -itemSize * 2, 1 to -itemSize, 2 to 0.dp)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_scrollToLastItem() {
+ lateinit var state: LazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollTo(3)
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, 0.dp)
+ state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_scrollToLastItemByDelta() {
+ lateinit var state: LazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(itemSize * 3)
+
+ rule.onNodeWithTag("0")
+ .assertIsNotDisplayed()
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+
+ rule.onNodeWithTag("3")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, 0.dp)
+ state.assertVisibleItems(1 to -itemSize * 2, 2 to -itemSize, 3 to 0.dp)
+ }
+ }
+
+ @Test
+ fun eachPaddingLargerParentSize_scrollTillTheEnd() {
+ // only the end content padding is displayed
+ lateinit var state: LazyGridState
+ rule.setContent {
+ state = rememberLazyGridState()
+ Box(modifier = Modifier.testTag(ContainerTag).size(itemSize * 1.5f)) {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ state = state,
+ contentPadding = PaddingValues(vertical = itemSize * 2)
+ ) {
+ items(4) {
+ Box(Modifier.testTag("$it").size(itemSize))
+ }
+ }
+ }
+ }
+
+ state.scrollBy(
+ itemSize * 1.5f + // container size
+ itemSize * 2 + // start padding
+ itemSize * 3 // all items
+ )
+
+ rule.onNodeWithTag("3")
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ state.assertScrollPosition(3, itemSize * 3.5f)
+ state.assertVisibleItems(3 to -itemSize * 3.5f)
+ }
+ }
// @Test
// fun row_contentPaddingIsApplied() {
@@ -754,4 +1099,21 @@
assertThat(this@assertScrollPosition.firstVisibleItemIndex).isEqualTo(index)
assertThat(firstVisibleItemScrollOffset.toDp().value).isWithin(0.5f).of(offset.value)
}
+
+ private fun LazyGridState.assertLayoutInfoOffsetRange(from: Dp, to: Dp) = with(rule.density) {
+ assertThat(layoutInfo.viewportStartOffset to layoutInfo.viewportEndOffset)
+ .isEqualTo(from.roundToPx() to to.roundToPx())
+ }
+
+ private fun LazyGridState.assertVisibleItems(vararg expected: Pair<Int, Dp>) =
+ with(rule.density) {
+ assertThat(layoutInfo.visibleItemsInfo.map { it.index to it.offset.y })
+ .isEqualTo(expected.map { it.first to it.second.roundToPx() })
+ }
+
+ fun LazyGridState.scrollTo(index: Int) {
+ runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+ scrollToItem(index)
+ }
+ }
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
new file mode 100644
index 0000000..b84adbd
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsReverseLayoutTest.kt
@@ -0,0 +1,562 @@
+/*
+ * Copyright 2020 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.compose.foundation.lazy.grid
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.GridCells
+import androidx.compose.foundation.lazy.LazyGridState
+import androidx.compose.foundation.lazy.LazyVerticalGrid
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.list.scrollBy
+import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration
+import androidx.compose.foundation.lazy.rememberLazyGridState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(ExperimentalFoundationApi::class)
+class LazyGridsReverseLayoutTest {
+
+ private val ContainerTag = "ContainerTag"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private var itemSize: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSize = 50.toDp()
+ }
+ }
+
+ @Test
+ fun verticalGrid_reverseLayout() {
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ GridCells.Fixed(2),
+ Modifier.width(itemSize * 2),
+ reverseLayout = true
+ ) {
+ items(4) {
+ Box(Modifier.height(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun column_emitTwoElementsAsOneItem() {
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ GridCells.Fixed(2),
+ Modifier.width(itemSize * 2),
+ reverseLayout = true
+ ) {
+ items(4) {
+ Box(Modifier.height(itemSize).testTag((it * 2).toString()))
+ Box(Modifier.height(itemSize).testTag((it * 2 + 1).toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("4")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("5")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("6")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("7")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun verticalGrid_initialScrollPositionIs0() {
+ lateinit var state: LazyGridState
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ GridCells.Fixed(2),
+ reverseLayout = true,
+ state = rememberLazyGridState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..5).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun verticalGrid_scrollInWrongDirectionDoesNothing() {
+ lateinit var state: LazyGridState
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ reverseLayout = true,
+ state = rememberLazyGridState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ // we scroll down and as the scrolling is reversed it shouldn't affect anything
+ rule.onNodeWithTag(ContainerTag)
+ .scrollBy(y = itemSize, density = rule.density)
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun verticalGrid_scrollForwardHalfWay() {
+ lateinit var state: LazyGridState
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ reverseLayout = true,
+ state = rememberLazyGridState().also { state = it },
+ modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.requiredSize(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .scrollBy(y = -itemSize * 0.5f, density = rule.density)
+
+ val scrolled = rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+ }
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(-itemSize + scrolled)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(scrolled)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize + scrolled)
+ }
+
+ @Test
+ fun verticalGrid_scrollForwardTillTheEnd() {
+ lateinit var state: LazyGridState
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ GridCells.Fixed(1),
+ reverseLayout = true,
+ state = rememberLazyGridState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..3).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ // we scroll a bit more than it is possible just to make sure we would stop correctly
+ rule.onNodeWithTag(ContainerTag)
+ .scrollBy(y = -itemSize * 2.2f, density = rule.density)
+
+ rule.runOnIdle {
+ with(rule.density) {
+ val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+ itemSize * state.firstVisibleItemIndex
+ assertThat(realOffset).isEqualTo(itemSize * 2)
+ }
+ }
+
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ // @Test
+ // fun row_emitTwoElementsAsOneItem_positionedReversed() {
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true
+ // ) {
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("0"))
+ // Box(Modifier.requiredSize(itemSize).testTag("1"))
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // }
+
+ // @Test
+ // fun row_emitTwoItems_positionedReversed() {
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true
+ // ) {
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("0"))
+ // }
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("1"))
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // }
+
+ // @Test
+ // fun row_initialScrollPositionIs0() {
+ // lateinit var state: LazyListState
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true,
+ // state = rememberLazyListState().also { state = it },
+ // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+ // ) {
+ // items((0..2).toList()) {
+ // Box(Modifier.requiredSize(itemSize).testTag("$it"))
+ // }
+ // }
+ // }
+
+ // rule.runOnIdle {
+ // assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ // assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ // }
+ // }
+
+ // @Test
+ // fun row_scrollInWrongDirectionDoesNothing() {
+ // lateinit var state: LazyListState
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true,
+ // state = rememberLazyListState().also { state = it },
+ // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+ // ) {
+ // items((0..2).toList()) {
+ // Box(Modifier.requiredSize(itemSize).testTag("$it"))
+ // }
+ // }
+ // }
+
+ // // we scroll down and as the scrolling is reversed it shouldn't affect anything
+ // rule.onNodeWithTag(ContainerTag)
+ // .scrollBy(x = itemSize, density = rule.density)
+
+ // rule.runOnIdle {
+ // assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ // assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // }
+
+ // @Test
+ // fun row_scrollForwardHalfWay() {
+ // lateinit var state: LazyListState
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true,
+ // state = rememberLazyListState().also { state = it },
+ // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+ // ) {
+ // items((0..2).toList()) {
+ // Box(Modifier.requiredSize(itemSize).testTag("$it"))
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag(ContainerTag)
+ // .scrollBy(x = -itemSize * 0.5f, density = rule.density)
+
+ // val scrolled = rule.runOnIdle {
+ // assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ // assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ // with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+ // }
+
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(-itemSize + scrolled)
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(scrolled)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(itemSize + scrolled)
+ // }
+
+ // @Test
+ // fun row_scrollForwardTillTheEnd() {
+ // lateinit var state: LazyListState
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = true,
+ // state = rememberLazyListState().also { state = it },
+ // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+ // ) {
+ // items((0..3).toList()) {
+ // Box(Modifier.requiredSize(itemSize).testTag("$it"))
+ // }
+ // }
+ // }
+
+ // // we scroll a bit more than it is possible just to make sure we would stop correctly
+ // rule.onNodeWithTag(ContainerTag)
+ // .scrollBy(x = -itemSize * 2.2f, density = rule.density)
+
+ // rule.runOnIdle {
+ // with(rule.density) {
+ // val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+ // itemSize * state.firstVisibleItemIndex
+ // assertThat(realOffset).isEqualTo(itemSize * 2)
+ // }
+ // }
+
+ // rule.onNodeWithTag("3")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // }
+
+ // @Test
+ // fun row_rtl_emitTwoElementsAsOneItem_positionedReversed() {
+ // rule.setContentWithTestViewConfiguration {
+ // CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ // LazyRow(
+ // reverseLayout = true
+ // ) {
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("0"))
+ // Box(Modifier.requiredSize(itemSize).testTag("1"))
+ // }
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // }
+
+ // @Test
+ // fun row_rtl_emitTwoItems_positionedReversed() {
+ // rule.setContentWithTestViewConfiguration {
+ // CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ // LazyRow(
+ // reverseLayout = true
+ // ) {
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("0"))
+ // }
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("1"))
+ // }
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // }
+
+ // @Test
+ // fun row_rtl_scrollForwardHalfWay() {
+ // lateinit var state: LazyListState
+ // rule.setContentWithTestViewConfiguration {
+ // CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ // LazyRow(
+ // reverseLayout = true,
+ // state = rememberLazyListState().also { state = it },
+ // modifier = Modifier.requiredSize(itemSize * 2).testTag(ContainerTag)
+ // ) {
+ // items((0..2).toList()) {
+ // Box(Modifier.requiredSize(itemSize).testTag("$it"))
+ // }
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag(ContainerTag)
+ // .scrollBy(x = itemSize * 0.5f, density = rule.density)
+
+ // val scrolled = rule.runOnIdle {
+ // assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ // assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ // with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+ // }
+
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(-scrolled)
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize - scrolled)
+ // rule.onNodeWithTag("2")
+ // .assertLeftPositionInRootIsEqualTo(itemSize * 2 - scrolled)
+ // }
+
+ @Test
+ fun verticalGrid_whenParameterChanges() {
+ var reverse by mutableStateOf(true)
+ rule.setContentWithTestViewConfiguration {
+ LazyVerticalGrid(
+ GridCells.Fixed(2),
+ Modifier.width(itemSize * 2),
+ reverseLayout = reverse
+ ) {
+ items(4) {
+ Box(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+
+ rule.runOnIdle {
+ reverse = false
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ // @Test
+ // fun row_whenParameterChanges() {
+ // var reverse by mutableStateOf(true)
+ // rule.setContentWithTestViewConfiguration {
+ // LazyRow(
+ // reverseLayout = reverse
+ // ) {
+ // item {
+ // Box(Modifier.requiredSize(itemSize).testTag("0"))
+ // Box(Modifier.requiredSize(itemSize).testTag("1"))
+ // }
+ // }
+ // }
+
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+
+ // rule.runOnIdle {
+ // reverse = false
+ // }
+
+ // rule.onNodeWithTag("0")
+ // .assertLeftPositionInRootIsEqualTo(0.dp)
+ // rule.onNodeWithTag("1")
+ // .assertLeftPositionInRootIsEqualTo(itemSize)
+ // }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
index 1813721..4c005cb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
@@ -41,6 +41,9 @@
* @param modifier the modifier to apply to this layout
* @param state the state object to be used to control or observe the list's state
* @param contentPadding specify a padding around the whole content
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the end to the start and [LazyGridState.firstVisibleItemIndex] == 0 will mean
+ * the first item is located at the end.
* @param verticalArrangement The vertical arrangement of the layout's children
* @param horizontalArrangement The horizontal arrangement of the layout's children
* @param flingBehavior logic describing fling behavior
@@ -55,7 +58,9 @@
modifier: Modifier = Modifier,
state: LazyGridState = rememberLazyGridState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
- verticalArrangement: Arrangement.Vertical = Arrangement.Top,
+ reverseLayout: Boolean = false,
+ verticalArrangement: Arrangement.Vertical =
+ if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
@@ -76,6 +81,7 @@
modifier = modifier,
state = state,
contentPadding = contentPadding,
+ reverseLayout = reverseLayout,
horizontalArrangement = horizontalArrangement,
verticalArrangement = verticalArrangement,
flingBehavior = flingBehavior,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 9d88a96..7cd6054 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -29,7 +29,6 @@
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyGridScope
import androidx.compose.foundation.lazy.LazyGridState
import androidx.compose.foundation.lazy.layout.LazyLayout
@@ -42,12 +41,15 @@
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.offset
import androidx.compose.ui.util.fastForEach
@OptIn(ExperimentalFoundationApi::class)
@@ -140,8 +142,7 @@
state = state,
overScrollController = overScrollController,
enabled = userScrollEnabled
- )
- .padding(contentPadding),
+ ),
prefetchPolicy = state.prefetchPolicy,
measurePolicy = measurePolicy,
itemsProvider = { stateOfItemsProvider.value }
@@ -200,6 +201,23 @@
LazyMeasurePolicy { placeablesProvider, constraints ->
constraints.assertNotNestingScrollableContainers(isVertical)
+ // resolve content paddings
+ val startPadding = contentPadding.calculateStartPadding(layoutDirection).roundToPx()
+ val endPadding = contentPadding.calculateEndPadding(layoutDirection).roundToPx()
+ val topPadding = contentPadding.calculateTopPadding().roundToPx()
+ val bottomPadding = contentPadding.calculateBottomPadding().roundToPx()
+ val totalVerticalPadding = topPadding + bottomPadding
+ val totalHorizontalPadding = startPadding + endPadding
+ val totalMainAxisPadding = if (isVertical) totalVerticalPadding else totalHorizontalPadding
+ val beforeContentPadding = when {
+ isVertical && !reverseLayout -> topPadding
+ isVertical && reverseLayout -> bottomPadding
+ !isVertical && !reverseLayout -> startPadding
+ else -> endPadding // !isVertical && reverseLayout
+ }
+ val afterContentPadding = totalMainAxisPadding - beforeContentPadding
+ val contentConstraints = constraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
+
val itemsProvider = stateOfItemsProvider.value
state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
@@ -211,21 +229,6 @@
val resolvedSlotsPerLine = slotsPerLine(constraints)
spanLayoutProvider.slotsPerLine = resolvedSlotsPerLine
- val rawBeforeContentPadding = if (isVertical) {
- contentPadding.calculateTopPadding()
- } else {
- contentPadding.calculateStartPadding(layoutDirection)
- }.roundToPx()
- val rawAfterContentPadding = if (isVertical) {
- contentPadding.calculateBottomPadding()
- } else {
- contentPadding.calculateEndPadding(layoutDirection)
- }.roundToPx()
- val beforeContentPadding =
- if (reverseLayout) rawAfterContentPadding else rawBeforeContentPadding
- val afterContentPadding =
- if (reverseLayout) rawBeforeContentPadding else rawAfterContentPadding
- val mainAxisMaxSize = (if (isVertical) constraints.maxHeight else constraints.maxWidth)
val spaceBetweenLinesDp = if (isVertical) {
requireNotNull(verticalArrangement).spacing
} else {
@@ -256,12 +259,13 @@
layoutDirection = layoutDirection,
beforeContentPadding = beforeContentPadding,
afterContentPadding = afterContentPadding,
+ visualOffset = IntOffset(startPadding, topPadding),
placeables = placeables,
placementAnimator = placementAnimator
)
}
val measuredLineProvider = LazyMeasuredLineProvider(
- constraints,
+ contentConstraints,
isVertical,
resolvedSlotsPerLine,
spaceBetweenSlots,
@@ -275,6 +279,7 @@
items = items,
spans = spans,
isVertical = isVertical,
+ reverseLayout = reverseLayout,
slotsPerLine = resolvedSlotsPerLine,
layoutDirection = layoutDirection,
mainAxisSpacing = mainAxisSpacing,
@@ -295,6 +300,13 @@
result
}
+ // can be negative if the content padding is larger than the max size from constraints
+ val mainAxisAvailableSize = if (isVertical) {
+ constraints.maxHeight - totalVerticalPadding
+ } else {
+ constraints.maxWidth - totalHorizontalPadding
+ }
+
val firstVisibleLineIndex: LineIndex
val firstVisibleLineScrollOffset: Int
if (state.firstVisibleItemIndexNonObservable.value < itemsCount || itemsCount <= 0) {
@@ -312,14 +324,14 @@
itemsCount = itemsCount,
measuredLineProvider = measuredLineProvider,
measuredItemProvider = measuredItemProvider,
- mainAxisMaxSize = mainAxisMaxSize,
+ mainAxisAvailableSize = mainAxisAvailableSize,
slotsPerLine = resolvedSlotsPerLine,
beforeContentPadding = beforeContentPadding,
afterContentPadding = afterContentPadding,
firstVisibleLineIndex = firstVisibleLineIndex,
firstVisibleLineScrollOffset = firstVisibleLineScrollOffset,
scrollToBeConsumed = state.scrollToBeConsumed,
- constraints = constraints,
+ constraints = contentConstraints,
isVertical = isVertical,
verticalArrangement = verticalArrangement,
horizontalArrangement = horizontalArrangement,
@@ -327,36 +339,43 @@
density = this,
layoutDirection = layoutDirection,
placementAnimator = placementAnimator,
- layout = { width, height, placement -> layout(width, height, emptyMap(), placement) }
+ layout = { width, height, placement ->
+ layout(
+ constraints.constrainWidth(width + totalHorizontalPadding),
+ constraints.constrainHeight(height + totalVerticalPadding),
+ emptyMap(),
+ placement
+ )
+ }
).also {
state.applyMeasureResult(it)
- refreshOverScrollInfo(overScrollController, it, contentPadding)
+ refreshOverScrollInfo(
+ overScrollController,
+ it,
+ constraints,
+ totalHorizontalPadding,
+ totalVerticalPadding
+ )
}
}
}
-private fun IntrinsicMeasureScope.refreshOverScrollInfo(
+private fun refreshOverScrollInfo(
overScrollController: OverScrollController,
result: LazyGridMeasureResult,
- contentPadding: PaddingValues
+ constraints: Constraints,
+ totalHorizontalPadding: Int,
+ totalVerticalPadding: Int
) {
- val verticalPadding =
- contentPadding.calculateTopPadding() +
- contentPadding.calculateBottomPadding()
-
- val horizontalPadding =
- contentPadding.calculateLeftPadding(layoutDirection) +
- contentPadding.calculateRightPadding(layoutDirection)
-
val canScrollForward = result.canScrollForward
val canScrollBackward = (result.firstVisibleLine?.items?.firstOrNull() ?: 0) != 0 ||
result.firstVisibleLineScrollOffset != 0
overScrollController.refreshContainerInfo(
Size(
- result.width.toFloat() + horizontalPadding.roundToPx(),
- result.height.toFloat() + verticalPadding.roundToPx()
+ constraints.constrainWidth(result.width + totalHorizontalPadding).toFloat(),
+ constraints.constrainHeight(result.height + totalVerticalPadding).toFloat()
),
canScrollForward || canScrollBackward
)
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index 0fc0811..3e7e34b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -28,6 +28,7 @@
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.util.fastForEach
import kotlin.math.abs
+import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sign
@@ -40,7 +41,7 @@
itemsCount: Int,
measuredLineProvider: LazyMeasuredLineProvider,
measuredItemProvider: LazyMeasuredItemProvider,
- mainAxisMaxSize: Int,
+ mainAxisAvailableSize: Int,
slotsPerLine: Int,
beforeContentPadding: Int,
afterContentPadding: Int,
@@ -100,7 +101,7 @@
// define min and max offsets (min offset currently includes beforeContentPadding)
val minOffset = -beforeContentPadding
- val maxOffset = mainAxisMaxSize
+ val maxOffset = mainAxisAvailableSize
// we had scrolled backward or we compose items in the start padding area, which means
// items before current firstLineScrollOffset should be visible. compose them and update
@@ -123,25 +124,28 @@
currentFirstLineScrollOffset += beforeContentPadding
var index = currentFirstLineIndex
- val maxMainAxis = maxOffset + afterContentPadding
- var mainAxisUsed = -currentFirstLineScrollOffset
+ val maxMainAxis = (maxOffset + afterContentPadding).coerceAtLeast(0)
+ var currentMainAxisOffset = -currentFirstLineScrollOffset
// first we need to skip lines we already composed while composing backward
visibleLines.fastForEach {
index++
- mainAxisUsed += it.mainAxisSizeWithSpacings
+ currentMainAxisOffset += it.mainAxisSizeWithSpacings
}
- // then composing visible lines forward until we fill the whole viewport
- while (mainAxisUsed <= maxMainAxis) {
+ // then composing visible lines forward until we fill the whole viewport.
+ // we want to have at least one line in visibleItems even if in fact all the items are
+ // offscreen, this can happen if the content padding is larger than the available size.
+ while (currentMainAxisOffset <= maxMainAxis || visibleLines.isEmpty()) {
val measuredLine = measuredLineProvider.getAndMeasure(index)
if (measuredLine.isEmpty()) {
--index
break
}
- mainAxisUsed += measuredLine.mainAxisSizeWithSpacings
- if (mainAxisUsed <= minOffset) {
+ currentMainAxisOffset += measuredLine.mainAxisSizeWithSpacings
+ if (currentMainAxisOffset <= minOffset &&
+ measuredLine.items.last().index.value != itemsCount - 1) {
// this line is offscreen and will not be placed. advance firstVisibleLineIndex
currentFirstLineIndex = index + 1
currentFirstLineScrollOffset -= measuredLine.mainAxisSizeWithSpacings
@@ -153,10 +157,10 @@
// we didn't fill the whole viewport with lines starting from firstVisibleLineIndex.
// lets try to scroll back if we have enough lines before firstVisibleLineIndex.
- if (mainAxisUsed < maxOffset) {
- val toScrollBack = maxOffset - mainAxisUsed
+ if (currentMainAxisOffset < maxOffset) {
+ val toScrollBack = maxOffset - currentMainAxisOffset
currentFirstLineScrollOffset -= toScrollBack
- mainAxisUsed += toScrollBack
+ currentMainAxisOffset += toScrollBack
while (currentFirstLineScrollOffset < 0 && currentFirstLineIndex > LineIndex(0)) {
val previousIndex = LineIndex(currentFirstLineIndex.value - 1)
val measuredLine = measuredLineProvider.getAndMeasure(previousIndex)
@@ -167,7 +171,7 @@
scrollDelta += toScrollBack
if (currentFirstLineScrollOffset < 0) {
scrollDelta += currentFirstLineScrollOffset
- mainAxisUsed += currentFirstLineScrollOffset
+ currentMainAxisOffset += currentFirstLineScrollOffset
currentFirstLineScrollOffset = 0
}
}
@@ -186,7 +190,7 @@
// the initial offset for lines from visibleLines list
val visibleLinesScrollOffset = -currentFirstLineScrollOffset
- var firstLine = visibleLines.firstOrNull()
+ var firstLine = visibleLines.first()
// even if we compose lines to fill before content padding we should ignore lines fully
// located there for the state's scroll position calculation (first line + first offset)
@@ -202,16 +206,23 @@
}
}
- val layoutWidth =
- if (isVertical) constraints.maxWidth else constraints.constrainWidth(mainAxisUsed)
- val layoutHeight =
- if (isVertical) constraints.constrainHeight(mainAxisUsed) else constraints.maxHeight
+ val layoutWidth = if (isVertical) {
+ constraints.maxWidth
+ } else {
+ constraints.constrainWidth(currentMainAxisOffset)
+ }
+ val layoutHeight = if (isVertical) {
+ constraints.constrainHeight(currentMainAxisOffset)
+ } else {
+ constraints.maxHeight
+ }
val positionedItems = calculateItemsOffsets(
lines = visibleLines,
layoutWidth = layoutWidth,
layoutHeight = layoutHeight,
- usedMainAxisSize = mainAxisUsed,
+ finalMainAxisOffset = currentMainAxisOffset,
+ maxOffset = maxOffset,
firstLineScrollOffset = visibleLinesScrollOffset,
isVertical = isVertical,
verticalArrangement = verticalArrangement,
@@ -231,12 +242,12 @@
measuredItemProvider = measuredItemProvider
)
- val maximumVisibleOffset = minOf(mainAxisUsed, mainAxisMaxSize) + afterContentPadding
+ val maximumVisibleOffset = minOf(currentMainAxisOffset, maxOffset) + afterContentPadding
return LazyGridMeasureResult(
firstVisibleLine = firstLine,
firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
- canScrollForward = mainAxisUsed > maxOffset,
+ canScrollForward = currentMainAxisOffset > maxOffset,
consumedScroll = consumedScroll,
measureResult = layout(layoutWidth, layoutHeight) {
positionedItems.fastForEach { it.place(this) }
@@ -259,7 +270,8 @@
lines: List<LazyMeasuredLine>,
layoutWidth: Int,
layoutHeight: Int,
- usedMainAxisSize: Int,
+ finalMainAxisOffset: Int,
+ maxOffset: Int,
firstLineScrollOffset: Int,
isVertical: Boolean,
verticalArrangement: Arrangement.Vertical?,
@@ -269,7 +281,7 @@
layoutDirection: LayoutDirection
): MutableList<LazyGridPositionedItem> {
val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
- val hasSpareSpace = usedMainAxisSize < mainAxisLayoutSize
+ val hasSpareSpace = finalMainAxisOffset < min(mainAxisLayoutSize, maxOffset)
if (hasSpareSpace) {
check(firstLineScrollOffset == 0)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt
index facf78b..f54b0a9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredItem.kt
@@ -44,7 +44,12 @@
private val beforeContentPadding: Int,
private val afterContentPadding: Int,
val placeables: Array<LazyLayoutPlaceable>,
- private val placementAnimator: LazyGridItemPlacementAnimator
+ private val placementAnimator: LazyGridItemPlacementAnimator,
+ /**
+ * The offset which shouldn't affect any calculations but needs to be applied for the final
+ * value passed into the place() call.
+ */
+ private val visualOffset: IntOffset
) {
/**
* Main axis size of the item - the max main axis size of the placeables.
@@ -92,7 +97,7 @@
rawMainAxisOffset
}
val crossAxisLayoutSize = if (isVertical) layoutWidth else layoutHeight
- val crossAxisOffset = if (layoutDirection == LayoutDirection.Rtl && isVertical) {
+ val crossAxisOffset = if (isVertical && layoutDirection == LayoutDirection.Rtl) {
crossAxisLayoutSize - rawCrossAxisOffset - crossAxisSize
} else {
rawCrossAxisOffset
@@ -115,7 +120,12 @@
}
return LazyGridPositionedItem(
- offset = placeableOffset,
+ offset = if (isVertical) {
+ IntOffset(rawCrossAxisOffset, rawMainAxisOffset)
+ } else {
+ IntOffset(rawMainAxisOffset, rawCrossAxisOffset)
+ },
+ placeableOffset = placeableOffset,
index = index.value,
key = key,
row = row,
@@ -136,7 +146,8 @@
if (!reverseLayout) afterContentPadding else beforeContentPadding,
isVertical = isVertical,
wrappers = wrappers,
- placementAnimator = placementAnimator
+ placementAnimator = placementAnimator,
+ visualOffset = visualOffset
)
}
}
@@ -144,6 +155,7 @@
@OptIn(ExperimentalFoundationApi::class)
internal class LazyGridPositionedItem(
override val offset: IntOffset,
+ val placeableOffset: IntOffset,
override val index: Int,
override val key: Any,
override val row: Int,
@@ -155,7 +167,8 @@
private val maxMainAxisOffset: Int,
private val isVertical: Boolean,
private val wrappers: List<LazyGridPlaceableWrapper>,
- private val placementAnimator: LazyGridItemPlacementAnimator
+ private val placementAnimator: LazyGridItemPlacementAnimator,
+ private val visualOffset: IntOffset
) : LazyGridItemInfo {
val placeablesCount: Int get() = wrappers.size
@@ -187,16 +200,16 @@
val maxOffset = maxMainAxisOffset
val offset = if (getAnimationSpec(index) != null) {
placementAnimator.getAnimatedOffset(
- key, index, minOffset, maxOffset, offset
+ key, index, minOffset, maxOffset, placeableOffset
)
} else {
- offset
+ placeableOffset
}
if (offset.mainAxis > minOffset && offset.mainAxis < maxOffset) {
if (isVertical) {
- placeable.placeWithLayer(offset)
+ placeable.placeWithLayer(offset + visualOffset)
} else {
- placeable.placeRelativeWithLayer(offset)
+ placeable.placeRelativeWithLayer(offset + visualOffset)
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt
index d0e5099..b3753f6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLine.kt
@@ -30,6 +30,7 @@
val items: Array<LazyMeasuredItem>,
private val spans: List<GridItemSpan>,
private val isVertical: Boolean,
+ private val reverseLayout: Boolean,
private val slotsPerLine: Int,
private val layoutDirection: LayoutDirection,
/**
@@ -75,8 +76,7 @@
var usedSpan = 0
return items.mapIndexed { itemIndex, item ->
val span = spans[itemIndex].currentLineSpan
- // TODO(b/215572963): check if we need to consider reverseLayout here
- val startSlot = if (layoutDirection == LayoutDirection.Rtl && isVertical) {
+ val startSlot = if (layoutDirection == LayoutDirection.Rtl) {
slotsPerLine - usedSpan - span
} else {
usedSpan
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
index 7a7bf2a..b5a8bbf 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyMeasuredLineProvider.kt
@@ -72,6 +72,7 @@
var startSlot = 0
val items = Array(lineItemsCount) {
val span = lineConfiguration.spans[it].currentLineSpan
+ // TODO(vadimsemenov): consider reverseLayout when calculating childConstraints
val constraints = childConstraints(startSlot, span)
measuredItemProvider.getAndMeasure(
ItemIndex(lineConfiguration.firstItemIndex + it),
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
index 0e68e64..dab01cc 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AlertDialogTest.kt
@@ -19,6 +19,7 @@
import android.os.Build
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.tokens.DialogTokens
@@ -316,6 +317,62 @@
"padding between the text and the button"
)
}
+
+ @Test
+ fun alertDialog_positioningWithLazyColumnText() {
+ rule.setMaterialContent(lightColorScheme()) {
+ AlertDialog(
+ onDismissRequest = {},
+ title = { Text(text = "Title", modifier = Modifier.testTag(TitleTestTag)) },
+ text = {
+ LazyColumn(modifier = Modifier.testTag(TextTestTag)) {
+ items(100) {
+ Text(
+ text = "Message!"
+ )
+ }
+ }
+ },
+ confirmButton = {},
+ dismissButton = {
+ TextButton(
+ onClick = { /* doSomething() */ },
+ Modifier.testTag(DismissButtonTestTag).semantics(mergeDescendants = true) {}
+ ) {
+ Text("Dismiss")
+ }
+ }
+ )
+ }
+
+ val dialogBounds = rule.onNode(isDialog()).getUnclippedBoundsInRoot()
+ val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
+ val textBounds = rule.onNodeWithTag(TextTestTag).getUnclippedBoundsInRoot()
+ val dismissBtBounds = rule.onNodeWithTag(DismissButtonTestTag).getUnclippedBoundsInRoot()
+
+ rule.onNodeWithTag(TitleTestTag)
+ // Title should 24dp from the left.
+ .assertLeftPositionInRootIsEqualTo(24.dp)
+ // Title should be 24dp from the top.
+ .assertTopPositionInRootIsEqualTo(24.dp)
+
+ rule.onNodeWithTag(TextTestTag)
+ // Text should be 24dp from the start.
+ .assertLeftPositionInRootIsEqualTo(24.dp)
+ // Text should be 16dp below the title.
+ .assertTopPositionInRootIsEqualTo(titleBounds.bottom + 16.dp)
+
+ rule.onNodeWithTag(DismissButtonTestTag)
+ // Dismiss button should be 24dp from the right.
+ .assertLeftPositionInRootIsEqualTo(dialogBounds.right - 24.dp - dismissBtBounds.width)
+ // Buttons should be 18dp from the bottom (test button default height is 48dp).
+ .assertTopPositionInRootIsEqualTo(dialogBounds.bottom - 18.dp - 48.dp)
+
+ (dismissBtBounds.top - textBounds.bottom).assertIsEqualTo(
+ 18.dp,
+ "padding between the text and the button"
+ )
+ }
}
private val AlertDialogMinWidth = 280.dp
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt
index 4261656..ec056ca 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationDrawerTest.kt
@@ -69,6 +69,8 @@
rule.mainClock.advanceTimeBy(100_000L)
}
+ val NavigationDrawerWidth = NavigationDrawerTokens.ContainerWidth
+
@Test
fun navigationDrawer_testOffset_whenOpen() {
rule.setMaterialContent(lightColorScheme()) {
@@ -99,9 +101,8 @@
)
}
- val width = rule.rootWidth()
rule.onNodeWithTag("content")
- .assertLeftPositionInRootIsEqualTo(-width)
+ .assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
}
@Test
@@ -156,10 +157,8 @@
)
}
- val width = rule.rootWidth()
-
// Drawer should start in closed state
- rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-width)
+ rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
// When the drawer state is set to Opened
drawerState.open()
@@ -169,7 +168,7 @@
// When the drawer state is set to Closed
drawerState.close()
// Then the drawer should be closed
- rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-width)
+ rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
}
@Test
@@ -187,10 +186,8 @@
)
}
- val width = rule.rootWidth()
-
// Drawer should start in closed state
- rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-width)
+ rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
// When the drawer state is set to Opened
drawerState.animateTo(DrawerValue.Open, TweenSpec())
@@ -200,7 +197,7 @@
// When the drawer state is set to Closed
drawerState.animateTo(DrawerValue.Closed, TweenSpec())
// Then the drawer should be closed
- rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-width)
+ rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
}
@Test
@@ -218,10 +215,8 @@
)
}
- val width = rule.rootWidth()
-
// Drawer should start in closed state
- rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-width)
+ rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
// When the drawer state is set to Opened
drawerState.snapTo(DrawerValue.Open)
@@ -231,7 +226,7 @@
// When the drawer state is set to Closed
drawerState.snapTo(DrawerValue.Closed)
// Then the drawer should be closed
- rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-width)
+ rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
}
@Test
@@ -524,7 +519,7 @@
.performSemanticsAction(SemanticsActions.OnClick)
// Then the drawer should be closed
- rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-rule.rootWidth())
+ rule.onNodeWithTag(DrawerTestTag).assertLeftPositionInRootIsEqualTo(-NavigationDrawerWidth)
topNode = rule.onNodeWithTag(topTag).fetchSemanticsNode()
assertEquals(2, topNode.children.size)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt
index 1d28bed..0a7a61f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt
@@ -100,6 +100,7 @@
ProvideTextStyle(textStyle) {
Box(
Modifier
+ .weight(weight = 1f, fill = false)
.padding(TextPadding)
.align(Alignment.Start)
) {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
index 8a3290b..44707f5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
@@ -22,7 +22,6 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
@@ -53,9 +52,9 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
-import kotlin.math.roundToInt
/**
* Possible values of [DrawerState].
@@ -256,80 +255,65 @@
content: @Composable () -> Unit
) {
val scope = rememberCoroutineScope()
- BoxWithConstraints(modifier.fillMaxSize()) {
- val navigationDrawerConstraints = constraints
- // TODO : think about Infinite max bounds case
- if (!navigationDrawerConstraints.hasBoundedWidth) {
- throw IllegalStateException("Drawer shouldn't have infinite width")
+ val minValue = -with(LocalDensity.current) { NavigationDrawerTokens.ContainerWidth.toPx() }
+ val maxValue = 0f
+
+ val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ Box(
+ modifier.fillMaxSize().swipeable(
+ state = drawerState.swipeableState,
+ anchors = anchors,
+ thresholds = { _, _ -> FractionalThreshold(0.5f) },
+ orientation = Orientation.Horizontal,
+ enabled = gesturesEnabled,
+ reverseDirection = isRtl,
+ velocityThreshold = DrawerVelocityThreshold,
+ resistance = null
+ )
+ ) {
+ Box {
+ content()
}
-
- val minValue = -navigationDrawerConstraints.maxWidth.toFloat()
- val maxValue = 0f
-
- val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
- val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- Box(
- Modifier.swipeable(
- state = drawerState.swipeableState,
- anchors = anchors,
- thresholds = { _, _ -> FractionalThreshold(0.5f) },
- orientation = Orientation.Horizontal,
- enabled = gesturesEnabled,
- reverseDirection = isRtl,
- velocityThreshold = DrawerVelocityThreshold,
- resistance = null
- )
- ) {
- Box {
- content()
- }
- Scrim(
- open = drawerState.isOpen,
- onClose = {
- if (
- gesturesEnabled &&
- drawerState.swipeableState.confirmStateChange(DrawerValue.Closed)
- ) {
- scope.launch { drawerState.close() }
+ Scrim(
+ open = drawerState.isOpen,
+ onClose = {
+ if (
+ gesturesEnabled &&
+ drawerState.swipeableState.confirmStateChange(DrawerValue.Closed)
+ ) {
+ scope.launch { drawerState.close() }
+ }
+ },
+ fraction = {
+ calculateFraction(minValue, maxValue, drawerState.offset.value)
+ },
+ color = scrimColor
+ )
+ val navigationMenu = getString(Strings.NavigationMenu)
+ Surface(
+ modifier = Modifier
+ .sizeIn(maxWidth = NavigationDrawerTokens.ContainerWidth)
+ .offset { IntOffset(drawerState.offset.value.roundToInt(), 0) }
+ .semantics {
+ paneTitle = navigationMenu
+ if (drawerState.isOpen) {
+ dismiss {
+ if (
+ drawerState.swipeableState
+ .confirmStateChange(DrawerValue.Closed)
+ ) {
+ scope.launch { drawerState.close() }
+ }; true
+ }
}
},
- fraction = {
- calculateFraction(minValue, maxValue, drawerState.offset.value)
- },
- color = scrimColor
- )
- val navigationMenu = getString(Strings.NavigationMenu)
- Surface(
- modifier = with(LocalDensity.current) {
- Modifier
- .sizeIn(
- minWidth = navigationDrawerConstraints.minWidth.toDp(),
- minHeight = navigationDrawerConstraints.minHeight.toDp(),
- maxWidth = NavigationDrawerTokens.ContainerWidth,
- maxHeight = navigationDrawerConstraints.maxHeight.toDp()
- )
- }
- .offset { IntOffset(drawerState.offset.value.roundToInt(), 0) }
- .semantics {
- paneTitle = navigationMenu
- if (drawerState.isOpen) {
- dismiss {
- if (
- drawerState.swipeableState
- .confirmStateChange(DrawerValue.Closed)
- ) {
- scope.launch { drawerState.close() }
- }; true
- }
- }
- },
- shape = drawerShape,
- color = drawerContainerColor,
- contentColor = drawerContentColor,
- tonalElevation = drawerTonalElevation
- ) {
- Column(Modifier.fillMaxSize(), content = drawerContent)
- }
+ shape = drawerShape,
+ color = drawerContainerColor,
+ contentColor = drawerContentColor,
+ tonalElevation = drawerTonalElevation
+ ) {
+ Column(Modifier.fillMaxSize(), content = drawerContent)
}
}
}
diff --git a/core/core-ktx/api/current.txt b/core/core-ktx/api/current.txt
index 823f91d..c09a63a 100644
--- a/core/core-ktx/api/current.txt
+++ b/core/core-ktx/api/current.txt
@@ -274,6 +274,7 @@
public final class DrawableKt {
method public static android.graphics.Bitmap toBitmap(android.graphics.drawable.Drawable, optional @Px int width, optional @Px int height, optional android.graphics.Bitmap.Config? config);
+ method public static android.graphics.Bitmap? toBitmapOrNull(android.graphics.drawable.Drawable, optional @Px int width, optional @Px int height, optional android.graphics.Bitmap.Config? config);
method public static void updateBounds(android.graphics.drawable.Drawable, optional @Px int left, optional @Px int top, optional @Px int right, optional @Px int bottom);
}
diff --git a/core/core-ktx/api/public_plus_experimental_current.txt b/core/core-ktx/api/public_plus_experimental_current.txt
index 823f91d..c09a63a 100644
--- a/core/core-ktx/api/public_plus_experimental_current.txt
+++ b/core/core-ktx/api/public_plus_experimental_current.txt
@@ -274,6 +274,7 @@
public final class DrawableKt {
method public static android.graphics.Bitmap toBitmap(android.graphics.drawable.Drawable, optional @Px int width, optional @Px int height, optional android.graphics.Bitmap.Config? config);
+ method public static android.graphics.Bitmap? toBitmapOrNull(android.graphics.drawable.Drawable, optional @Px int width, optional @Px int height, optional android.graphics.Bitmap.Config? config);
method public static void updateBounds(android.graphics.drawable.Drawable, optional @Px int left, optional @Px int top, optional @Px int right, optional @Px int bottom);
}
diff --git a/core/core-ktx/api/restricted_current.txt b/core/core-ktx/api/restricted_current.txt
index 823f91d..c09a63a 100644
--- a/core/core-ktx/api/restricted_current.txt
+++ b/core/core-ktx/api/restricted_current.txt
@@ -274,6 +274,7 @@
public final class DrawableKt {
method public static android.graphics.Bitmap toBitmap(android.graphics.drawable.Drawable, optional @Px int width, optional @Px int height, optional android.graphics.Bitmap.Config? config);
+ method public static android.graphics.Bitmap? toBitmapOrNull(android.graphics.drawable.Drawable, optional @Px int width, optional @Px int height, optional android.graphics.Bitmap.Config? config);
method public static void updateBounds(android.graphics.drawable.Drawable, optional @Px int left, optional @Px int top, optional @Px int right, optional @Px int bottom);
}
diff --git a/core/core-ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt b/core/core-ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt
index 9c011eb..93703e8 100644
--- a/core/core-ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt
+++ b/core/core-ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt
@@ -24,6 +24,7 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
import org.junit.Test
@SmallTest
@@ -109,6 +110,22 @@
assertEquals(Color.RED, bitmap.getPixel(5, 5))
}
+ @Test
+ fun drawableOrNullEmptyBitmap() {
+ @Suppress("DEPRECATION") val drawable = BitmapDrawable()
+
+ val bitmap = drawable.toBitmapOrNull()
+ assertNull(bitmap)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun drawableEmptyBitmapThrowsIAE() {
+ @Suppress("DEPRECATION") val drawable = BitmapDrawable()
+
+ val bitmap = drawable.toBitmap()
+ assertNull(bitmap)
+ }
+
@Test fun drawableConfig() {
val drawable = object : ColorDrawable(Color.RED) {
override fun getIntrinsicWidth() = 10
diff --git a/core/core-ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt b/core/core-ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt
index be4a115..b82a760 100644
--- a/core/core-ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt
+++ b/core/core-ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt
@@ -38,6 +38,10 @@
* @param height Height of the desired bitmap. Defaults to [Drawable.getIntrinsicHeight].
* @param config Bitmap config of the desired bitmap. Null attempts to use the native config, if
* any. Defaults to [Config.ARGB_8888] otherwise.
+ * @throws IllegalArgumentException if the underlying drawable is a [BitmapDrawable] where
+ * [BitmapDrawable.getBitmap] returns `null` or the drawable cannot otherwise be represented as a
+ * bitmap
+ * @see toBitmapOrNull
*/
public fun Drawable.toBitmap(
@Px width: Int = intrinsicWidth,
@@ -45,6 +49,10 @@
config: Config? = null
): Bitmap {
if (this is BitmapDrawable) {
+ if (bitmap == null) {
+ // This is slightly better than returning an empty, zero-size bitmap.
+ throw IllegalArgumentException("bitmap is null")
+ }
if (config == null || bitmap.config == config) {
// Fast-path to return original. Bitmap.createScaledBitmap will do this check, but it
// involves allocation and two jumps into native code so we perform the check ourselves.
@@ -66,6 +74,34 @@
}
/**
+ * Returns a [Bitmap] representation of this [Drawable] or `null` if the drawable cannot be
+ * represented as a bitmap.
+ *
+ * If this instance is a [BitmapDrawable] and the [width], [height], and [config] match, the
+ * underlying [Bitmap] instance will be returned directly. If any of those three properties differ
+ * then a new [Bitmap] is created. For all other [Drawable] types, a new [Bitmap] is created.
+ *
+ * If the result of [BitmapDrawable.getBitmap] is `null` or the drawable cannot otherwise be
+ * represented as a bitmap, returns `null`.
+ *
+ * @param width Width of the desired bitmap. Defaults to [Drawable.getIntrinsicWidth].
+ * @param height Height of the desired bitmap. Defaults to [Drawable.getIntrinsicHeight].
+ * @param config Bitmap config of the desired bitmap. Null attempts to use the native config, if
+ * any. Defaults to [Config.ARGB_8888] otherwise.
+ * @see toBitmap
+ */
+public fun Drawable.toBitmapOrNull(
+ @Px width: Int = intrinsicWidth,
+ @Px height: Int = intrinsicHeight,
+ config: Config? = null
+): Bitmap? {
+ if (this is BitmapDrawable && bitmap == null) {
+ return null
+ }
+ return toBitmap(width, height, config)
+}
+
+/**
* Updates this drawable's bounds. This version of the method allows using named parameters
* to just set one or more axes.
*
diff --git a/development/publishScan.sh b/development/publishScan.sh
new file mode 100755
index 0000000..cd5fdd0
--- /dev/null
+++ b/development/publishScan.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+set -e
+
+buildId="$1"
+target="$2"
+
+function usage() {
+ echo "usage: $0 <buildId> <target>"
+ echo
+ echo "Downloads build scan information for the corresponding build and uploads it to the enterprise server configured in settings.gradle"
+ exit 1
+}
+
+if [ "$buildId" == "" ]; then
+ usage
+fi
+
+if [ "$target" == "" ]; then
+ usage
+fi
+
+function downloadScan() {
+ echo downloading build scan from $buildId $target
+ if [ "$target" == "androidx_incremental" ]; then
+ downloadPath="incremental/scan.zip"
+ else
+ downloadPath="scan.zip"
+ fi
+ cd /tmp
+ /google/data/ro/projects/android/fetch_artifact --bid $buildId --target $target "$downloadPath"
+ cd -
+}
+downloadScan
+
+# find scan dir
+if [ "$OUT_DIR" != "" ]; then
+ effectiveGradleUserHome="$OUT_DIR/.gradle"
+else
+ if [ "$GRADLE_USER_HOME" != "" ]; then
+ effectiveGradleUserHome="$GRADLE_USER_HOME"
+ else
+ effectiveGradleUserHome="$HOME/.gradle"
+ fi
+fi
+scanDir="$effectiveGradleUserHome/build-scan-data"
+
+function unzipScan() {
+ echo
+ echo unzipping build scan
+ rm -rf "$scanDir"
+ unzip /tmp/scan.zip -d "$scanDir"
+}
+unzipScan
+
+function uploadScan() {
+ log="$scanDir/upload-failure.log"
+ rm -f "$log"
+ echo
+ echo uploading build scan
+ ./gradlew buildScanPublishPrevious
+ sleep 2
+ if cat "$log" 2>/dev/null; then
+ echo upload failed
+ fi
+}
+uploadScan
diff --git a/glance/glance-appwidget-proto/src/main/proto/layout.proto b/glance/glance-appwidget-proto/src/main/proto/layout.proto
index 17c6a1c..3f07cbf 100644
--- a/glance/glance-appwidget-proto/src/main/proto/layout.proto
+++ b/glance/glance-appwidget-proto/src/main/proto/layout.proto
@@ -77,4 +77,6 @@
IMAGE = 13;
LINEAR_PROGRESS_INDICATOR = 14;
CIRCULAR_PROGRESS_INDICATOR = 15;
+ LAZY_VERTICAL_GRID = 16;
+ VERTICAL_GRID_ITEM = 17;
}
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index 2e8f665..c4268fc 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -190,6 +190,16 @@
package androidx.glance.appwidget.lazy {
+ public final inline class GridCells {
+ ctor public GridCells();
+ }
+
+ public static final class GridCells.Companion {
+ method public int Fixed(int count);
+ method public int getAdaptive();
+ property public final int Adaptive;
+ }
+
@androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyItemScope {
}
@@ -215,6 +225,25 @@
@kotlin.DslMarker public @interface LazyScopeMarker {
}
+ public final class LazyVerticalGridKt {
+ method @androidx.compose.runtime.Composable public static void LazyVerticalGrid(int gridCells, optional androidx.glance.GlanceModifier modifier, optional int horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyVerticalGridScope,kotlin.Unit> content);
+ method public static inline <T> void items(androidx.glance.appwidget.lazy.LazyVerticalGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void items(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ }
+
+ @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyVerticalGridScope {
+ method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
+ method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+ field public static final androidx.glance.appwidget.lazy.LazyVerticalGridScope.Companion Companion;
+ field public static final long UnspecifiedItemId = -9223372036854775808L; // 0x8000000000000000L
+ }
+
+ public static final class LazyVerticalGridScope.Companion {
+ field public static final long UnspecifiedItemId = -9223372036854775808L; // 0x8000000000000000L
+ }
+
}
package androidx.glance.appwidget.state {
@@ -245,6 +274,9 @@
public final class LazyListTranslatorKt {
}
+ public final class LazyVerticalGridTranslatorKt {
+ }
+
public final class LinearProgressIndicatorTranslatorKt {
}
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index 2e8f665..c4268fc 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -190,6 +190,16 @@
package androidx.glance.appwidget.lazy {
+ public final inline class GridCells {
+ ctor public GridCells();
+ }
+
+ public static final class GridCells.Companion {
+ method public int Fixed(int count);
+ method public int getAdaptive();
+ property public final int Adaptive;
+ }
+
@androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyItemScope {
}
@@ -215,6 +225,25 @@
@kotlin.DslMarker public @interface LazyScopeMarker {
}
+ public final class LazyVerticalGridKt {
+ method @androidx.compose.runtime.Composable public static void LazyVerticalGrid(int gridCells, optional androidx.glance.GlanceModifier modifier, optional int horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyVerticalGridScope,kotlin.Unit> content);
+ method public static inline <T> void items(androidx.glance.appwidget.lazy.LazyVerticalGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void items(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ }
+
+ @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyVerticalGridScope {
+ method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
+ method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+ field public static final androidx.glance.appwidget.lazy.LazyVerticalGridScope.Companion Companion;
+ field public static final long UnspecifiedItemId = -9223372036854775808L; // 0x8000000000000000L
+ }
+
+ public static final class LazyVerticalGridScope.Companion {
+ field public static final long UnspecifiedItemId = -9223372036854775808L; // 0x8000000000000000L
+ }
+
}
package androidx.glance.appwidget.state {
@@ -245,6 +274,9 @@
public final class LazyListTranslatorKt {
}
+ public final class LazyVerticalGridTranslatorKt {
+ }
+
public final class LinearProgressIndicatorTranslatorKt {
}
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index 2e8f665..c4268fc 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -190,6 +190,16 @@
package androidx.glance.appwidget.lazy {
+ public final inline class GridCells {
+ ctor public GridCells();
+ }
+
+ public static final class GridCells.Companion {
+ method public int Fixed(int count);
+ method public int getAdaptive();
+ property public final int Adaptive;
+ }
+
@androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyItemScope {
}
@@ -215,6 +225,25 @@
@kotlin.DslMarker public @interface LazyScopeMarker {
}
+ public final class LazyVerticalGridKt {
+ method @androidx.compose.runtime.Composable public static void LazyVerticalGrid(int gridCells, optional androidx.glance.GlanceModifier modifier, optional int horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyVerticalGridScope,kotlin.Unit> content);
+ method public static inline <T> void items(androidx.glance.appwidget.lazy.LazyVerticalGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void items(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method public static inline <T> void itemsIndexed(androidx.glance.appwidget.lazy.LazyVerticalGridScope, T![] items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,java.lang.Long> itemId, kotlin.jvm.functions.Function3<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ }
+
+ @androidx.glance.appwidget.lazy.LazyScopeMarker public interface LazyVerticalGridScope {
+ method public void item(optional long itemId, kotlin.jvm.functions.Function1<? super androidx.glance.appwidget.lazy.LazyItemScope,kotlin.Unit> content);
+ method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Long> itemId, kotlin.jvm.functions.Function2<? super androidx.glance.appwidget.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
+ field public static final androidx.glance.appwidget.lazy.LazyVerticalGridScope.Companion Companion;
+ field public static final long UnspecifiedItemId = -9223372036854775808L; // 0x8000000000000000L
+ }
+
+ public static final class LazyVerticalGridScope.Companion {
+ field public static final long UnspecifiedItemId = -9223372036854775808L; // 0x8000000000000000L
+ }
+
}
package androidx.glance.appwidget.state {
@@ -245,6 +274,9 @@
public final class LazyListTranslatorKt {
}
+ public final class LazyVerticalGridTranslatorKt {
+ }
+
public final class LinearProgressIndicatorTranslatorKt {
}
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
index 0c1ed98..b06545a 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
@@ -160,5 +160,18 @@
android:name="android.appwidget.provider"
android:resource="@xml/default_app_widget_info" />
</receiver>
+
+ <receiver
+ android:name="androidx.glance.appwidget.demos.VerticalGridAppWidgetReceiver"
+ android:label="@string/grid_widget_name"
+ android:enabled="@bool/glance_appwidget_available"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ </intent-filter>
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/default_app_widget_info" />
+ </receiver>
</application>
</manifest>
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
index d0d3248..c1e47c3 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
@@ -111,7 +111,6 @@
Row {
val modifier = GlanceModifier.fillMaxHeight().defaultWeight()
ScrollColumn(modifier)
- ScrollColumn(modifier)
if (width >= tripleColumn.width) {
ScrollColumn(modifier)
}
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/VerticalGridAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/VerticalGridAppWidget.kt
new file mode 100644
index 0000000..8ae60bc
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/VerticalGridAppWidget.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2021 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.glance.appwidget.demos
+
+import android.content.Intent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.glance.Button
+import androidx.glance.GlanceModifier
+import androidx.glance.LocalContext
+import androidx.glance.action.actionStartActivity
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.action.actionStartActivity
+import androidx.glance.appwidget.appWidgetBackground
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.lazy.LazyVerticalGrid
+import androidx.glance.appwidget.lazy.GridCells
+import androidx.glance.appwidget.lazy.itemsIndexed
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.fillMaxHeight
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.padding
+import androidx.glance.state.PreferencesGlanceStateDefinition
+import androidx.glance.text.Text
+
+class VerticalGridAppWidget : GlanceAppWidget() {
+
+ override val stateDefinition = PreferencesGlanceStateDefinition
+
+ @Composable
+ override fun Content() {
+ Column(
+ modifier = GlanceModifier.padding(R.dimen.external_padding).fillMaxSize()
+ .appWidgetBackground().cornerRadius(R.dimen.corner_radius),
+ verticalAlignment = Alignment.Vertical.CenterVertically,
+ horizontalAlignment = Alignment.Horizontal.CenterHorizontally
+ ) {
+ val modifier = GlanceModifier.fillMaxHeight().defaultWeight()
+ LazyVerticalGrid(gridCells = GridCells.Fixed(2), modifier) {
+ item { Text("LazyVerticalGrid") }
+ items(2, { it * 2L }) { index -> Text("Item $index") }
+ itemsIndexed(
+ listOf(
+ GlanceAppWidgetDemoActivity::class.java,
+ ListClickDestinationActivity::class.java
+ )
+ ) { index, activityClass ->
+ Row(
+ GlanceModifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.Horizontal.CenterHorizontally
+ ) {
+ Button(
+ text = "Activity ${index + 1}",
+ onClick = actionStartActivity(
+ Intent(
+ LocalContext.current,
+ activityClass
+ )
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+class VerticalGridAppWidgetReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget: GlanceAppWidget = VerticalGridAppWidget()
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
index 2f1891c..f7f27d8 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
@@ -29,4 +29,5 @@
<string name="error_widget_name">Error UI Widget</string>
<string name="scrollable_widget_name">Scrollable Widget</string>
<string name="image_widget_name">Image Widget</string>
+ <string name="grid_widget_name">Vertical Grid Widget</string>
</resources>
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt
index 0308350..19653ee 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/LayoutSelection.kt
@@ -89,6 +89,12 @@
Frame,
LinearProgressIndicator,
CircularProgressIndicator,
+ VerticalGridOneColumn,
+ VerticalGridTwoColumns,
+ VerticalGridThreeColumns,
+ VerticalGridFourColumns,
+ VerticalGridFiveColumns,
+ VerticalGridAutoFit,
// Note: Java keywords, such as 'switch', can't be used for layout ids.
Swtch,
@@ -113,6 +119,12 @@
LayoutType.ImageFillBounds to R.layout.image_fill_bounds,
LayoutType.LinearProgressIndicator to R.layout.linear_progress_indicator,
LayoutType.CircularProgressIndicator to R.layout.circular_progress_indicator,
+ LayoutType.VerticalGridOneColumn to R.layout.vertical_grid_one_column,
+ LayoutType.VerticalGridTwoColumns to R.layout.vertical_grid_two_columns,
+ LayoutType.VerticalGridThreeColumns to R.layout.vertical_grid_three_columns,
+ LayoutType.VerticalGridFourColumns to R.layout.vertical_grid_four_columns,
+ LayoutType.VerticalGridFiveColumns to R.layout.vertical_grid_five_columns,
+ LayoutType.VerticalGridAutoFit to R.layout.vertical_grid_auto_fit,
)
internal data class SizeSelector(
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
index 7d77f6a..90d3348 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
@@ -33,6 +33,8 @@
import androidx.glance.EmittableImage
import androidx.glance.appwidget.lazy.EmittableLazyColumn
import androidx.glance.appwidget.lazy.EmittableLazyListItem
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGrid
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGridListItem
import androidx.glance.appwidget.translators.setText
import androidx.glance.appwidget.translators.translateEmittableCheckBox
import androidx.glance.appwidget.translators.translateEmittableImage
@@ -42,6 +44,8 @@
import androidx.glance.appwidget.translators.translateEmittableText
import androidx.glance.appwidget.translators.translateEmittableLinearProgressIndicator
import androidx.glance.appwidget.translators.translateEmittableCircularProgressIndicator
+import androidx.glance.appwidget.translators.translateEmittableLazyVerticalGrid
+import androidx.glance.appwidget.translators.translateEmittableLazyVerticalGridListItem
import androidx.glance.layout.Alignment
import androidx.glance.layout.EmittableBox
import androidx.glance.layout.EmittableColumn
@@ -151,6 +155,12 @@
is EmittableCircularProgressIndicator -> {
translateEmittableCircularProgressIndicator(translationContext, element)
}
+ is EmittableLazyVerticalGrid -> {
+ translateEmittableLazyVerticalGrid(translationContext, element)
+ }
+ is EmittableLazyVerticalGridListItem -> {
+ translateEmittableLazyVerticalGridListItem(translationContext, element)
+ }
else -> {
throw IllegalArgumentException(
"Unknown element type ${element.javaClass.canonicalName}"
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt
index 98fe5bc..eb28846 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt
@@ -34,6 +34,8 @@
import androidx.glance.appwidget.lazy.EmittableLazyColumn
import androidx.glance.appwidget.lazy.EmittableLazyList
import androidx.glance.appwidget.lazy.EmittableLazyListItem
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGrid
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGridListItem
import androidx.glance.appwidget.proto.LayoutProto
import androidx.glance.appwidget.proto.LayoutProto.LayoutConfig
import androidx.glance.appwidget.proto.LayoutProto.LayoutDefinition
@@ -319,6 +321,8 @@
is EmittableImage -> LayoutProto.LayoutType.IMAGE
is EmittableLinearProgressIndicator -> LayoutProto.LayoutType.LINEAR_PROGRESS_INDICATOR
is EmittableCircularProgressIndicator -> LayoutProto.LayoutType.CIRCULAR_PROGRESS_INDICATOR
+ is EmittableLazyVerticalGrid -> LayoutProto.LayoutType.LAZY_VERTICAL_GRID
+ is EmittableLazyVerticalGridListItem -> LayoutProto.LayoutType.LIST_ITEM
is RemoteViewsRoot -> LayoutProto.LayoutType.REMOTE_VIEWS_ROOT
else ->
throw IllegalArgumentException("Unknown element type ${this.javaClass.canonicalName}")
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
index d1a31f4..b41b2c4 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
@@ -16,7 +16,6 @@
package androidx.glance.appwidget.lazy
-import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.glance.EmittableWithChildren
import androidx.glance.GlanceModifier
@@ -113,7 +112,6 @@
* Values between -2^63 and -2^62 are reserved for list items whose id has not been explicitly
* defined.
*/
-@VisibleForTesting
internal const val ReservedItemIdRangeEnd = -0x4_000_000_000_000_000L
@DslMarker
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
new file mode 100644
index 0000000..2a03358
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
@@ -0,0 +1,285 @@
+/*
+ * 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.glance.appwidget.lazy
+import androidx.compose.runtime.Composable
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceNode
+import androidx.glance.layout.Alignment
+import androidx.glance.EmittableWithChildren
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.wrapContentHeight
+
+/**
+ * The DSL implementation of a lazy grid layout. It composes only visible rows of the grid.
+ *
+ * @param gridCells the number of columns in the grid.
+ * @param modifier the modifier to apply to this layout
+ * @param horizontalAlignment the horizontal alignment applied to the items.
+ * @param content a block which describes the content. Inside this block you can use methods like
+ * [LazyVerticalGridScope.item] to add a single item or
+ * [LazyVerticalGridScope.items] to add a list of items.
+ */
+@Composable
+public fun LazyVerticalGrid(
+ gridCells: GridCells,
+ modifier: GlanceModifier = GlanceModifier,
+ horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+ content: LazyVerticalGridScope.() -> Unit
+) {
+ GlanceNode(
+ factory = ::EmittableLazyVerticalGrid,
+ update = {
+ this.set(gridCells) { this.gridCells = it }
+ this.set(modifier) { this.modifier = it }
+ this.set(horizontalAlignment) { this.horizontalAlignment = it }
+ },
+ content = applyVerticalGridScope(
+ Alignment(horizontalAlignment, Alignment.Vertical.CenterVertically),
+ content
+ )
+ )
+}
+
+internal fun applyVerticalGridScope(
+ alignment: Alignment,
+ content: LazyVerticalGridScope.() -> Unit
+): @Composable () -> Unit {
+ var nextImplicitItemId = ReservedItemIdRangeEnd
+ val itemList = mutableListOf<Pair<Long?, @Composable LazyItemScope.() -> Unit>>()
+ val listScopeImpl = object : LazyVerticalGridScope {
+ override fun item(itemId: Long, content: @Composable LazyItemScope.() -> Unit) {
+ require(itemId == LazyVerticalGridScope.UnspecifiedItemId ||
+ itemId > ReservedItemIdRangeEnd) {
+ """
+ You may not specify item ids less than $ReservedItemIdRangeEnd in a Glance
+ widget. These are reserved.
+ """.trimIndent()
+ }
+ itemList.add(itemId to content)
+ }
+
+ override fun items(
+ count: Int,
+ itemId: ((index: Int) -> Long),
+ itemContent: @Composable LazyItemScope.(index: Int) -> Unit
+ ) {
+ repeat(count) { index ->
+ item(itemId(index)) { itemContent(index) }
+ }
+ }
+ }
+ listScopeImpl.apply(content)
+ return {
+ itemList.forEach { (itemId, composable) ->
+ val id = itemId.takeIf {
+ it != LazyVerticalGridScope.UnspecifiedItemId } ?: nextImplicitItemId--
+ check(id != LazyVerticalGridScope.UnspecifiedItemId) {
+ "Implicit list item ids exhausted."
+ }
+ LazyVerticalGridItem(id, alignment) {
+ object : LazyItemScope { }.apply { composable() }
+ }
+ }
+ }
+}
+
+@Composable
+private fun LazyVerticalGridItem(
+ itemId: Long,
+ alignment: Alignment,
+ content: @Composable () -> Unit
+) {
+ GlanceNode(
+ factory = ::EmittableLazyVerticalGridListItem,
+ update = {
+ this.set(itemId) { this.itemId = it }
+ this.set(alignment) { this.alignment = it }
+ },
+ content = content
+ )
+}
+
+/**
+ * Receiver scope which is used by [LazyColumn].
+ */
+@LazyScopeMarker
+interface LazyVerticalGridScope {
+ /**
+ * Adds a single item.
+ *
+ * @param itemId a stable and unique id representing the item. The value may not be less than
+ * or equal to -2^62, as these values are reserved by the Glance API. Specifying the list
+ * item ids will maintain the scroll position through app widget updates in Android S and
+ * higher devices.
+ * @param content the content of the item
+ */
+ fun item(itemId: Long = UnspecifiedItemId, content: @Composable LazyItemScope.() -> Unit)
+
+ /**
+ * Adds a [count] of items.
+ *
+ * @param count the count of items
+ * @param itemId a factory of stable and unique ids representing the item. The value may not be
+ * less than or equal to -2^62, as these values are reserved by the Glance API. Specifying
+ * the list item ids will maintain the scroll position through app widget updates in Android
+ * S and higher devices.
+ * @param itemContent the content displayed by a single item
+ */
+ fun items(
+ count: Int,
+ itemId: ((index: Int) -> Long) = { UnspecifiedItemId },
+ itemContent: @Composable LazyItemScope.(index: Int) -> Unit
+ )
+
+ companion object {
+ const val UnspecifiedItemId = Long.MIN_VALUE
+ }
+}
+
+/**
+ * Adds a list of items.
+ *
+ * @param items the data list
+ * @param itemId a factory of stable and unique ids representing the item. The value may not be
+ * less than or equal to -2^62, as these values are reserved by the Glance API. Specifying
+ * the list item ids will maintain the scroll position through app widget updates in Android
+ * S and higher devices.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> LazyVerticalGridScope.items(
+ items: List<T>,
+ crossinline itemId: ((item: T) -> Long) = { LazyVerticalGridScope.UnspecifiedItemId },
+ crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
+) = items(items.size, { index: Int -> itemId(items[index]) }) {
+ itemContent(items[it])
+}
+
+/**
+ * Adds a list of items where the content of an item is aware of its index.
+ *
+ * @param items the data list
+ * @param itemId a factory of stable and unique ids representing the item. The value may not be
+ * less than or equal to -2^62, as these values are reserved by the Glance API. Specifying
+ * the list item ids will maintain the scroll position through app widget updates in Android
+ * S and higher devices.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> LazyVerticalGridScope.itemsIndexed(
+ items: List<T>,
+ crossinline itemId: ((index: Int, item: T) -> Long) =
+ { _, _ -> LazyVerticalGridScope.UnspecifiedItemId },
+ crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
+) = items(items.size, { index: Int -> itemId(index, items[index]) }) {
+ itemContent(it, items[it])
+}
+
+/**
+ * Adds an array of items.
+ *
+ * @param items the data array
+ * @param itemId a factory of stable and unique list item ids. Using the same itemId for multiple
+ * items in the array is not allowed. When you specify the itemId, the scroll position will be
+ * maintained based on the itemId, which means if you add/remove items before the current visible
+ * item the item with the given itemId will be kept as the first visible one.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> LazyVerticalGridScope.items(
+ items: Array<T>,
+ noinline itemId: ((item: T) -> Long) = { LazyVerticalGridScope.UnspecifiedItemId },
+ crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
+) = items(items.size, { index: Int -> itemId(items[index]) }) {
+ itemContent(items[it])
+}
+
+/**
+ * Adds a array of items where the content of an item is aware of its index.
+ *
+ * @param items the data array
+ * @param itemId a factory of stable and unique list item ids. Using the same itemId for multiple
+ * items in the array is not allowed. When you specify the itemId the scroll position will be
+ * maintained based on the itemId, which means if you add/remove items before the current visible
+ * item the item with the given itemId will be kept as the first visible one.
+ * @param itemContent the content displayed by a single item
+ */
+inline fun <T> LazyVerticalGridScope.itemsIndexed(
+ items: Array<T>,
+ noinline itemId: ((index: Int, item: T) -> Long) = {
+ _, _ -> LazyVerticalGridScope.UnspecifiedItemId
+ },
+ crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
+) = items(items.size, { index: Int -> itemId(index, items[index]) }) {
+ itemContent(it, items[it])
+}
+
+internal abstract class EmittableLazyVerticalGridList : EmittableWithChildren(
+ resetsDepthForChildren = true
+ ) {
+ override var modifier: GlanceModifier = GlanceModifier
+ public var horizontalAlignment: Alignment.Horizontal = Alignment.Start
+ public var gridCells: GridCells = GridCells.Adaptive
+
+ override fun toString() =
+ "EmittableLazyVerticalGridList(modifier=$modifier, " +
+ "horizontalAlignment=$horizontalAlignment, " +
+ "numColumn=$gridCells, " +
+ "children=[\n${childrenToString()}\n])"
+}
+
+internal class EmittableLazyVerticalGridListItem : EmittableWithChildren() {
+ override var modifier: GlanceModifier
+ get() = children.singleOrNull()?.modifier
+ ?: GlanceModifier.wrapContentHeight().fillMaxWidth()
+ set(_) {
+ throw IllegalAccessError(
+ "You cannot set the modifier of an EmittableLazyVerticalGridListItem"
+ )
+ }
+ var itemId: Long = 0
+ var alignment: Alignment = Alignment.CenterStart
+
+ override fun toString() =
+ "EmittableLazyVerticalGridListItem(" +
+ "modifier=$modifier, " +
+ "alignment=$alignment, " +
+ "children=[\n${childrenToString()}\n])"
+}
+
+internal class EmittableLazyVerticalGrid : EmittableLazyVerticalGridList()
+
+/**
+ * Defines the number of columns of the GridView.
+ */
+@Suppress("INLINE_CLASS_DEPRECATED")
+public inline class GridCells internal constructor(private val value: Int) {
+ override fun toString(): String {
+ return when (value) {
+ 0 -> "GridCells.Adaptive"
+ else -> "GridCells.Fixed($value)"
+ }
+ }
+
+ companion object {
+ fun Fixed(count: Int): GridCells {
+ require(count in 1..5) {
+ "Only counts from 1 to 5 are supported."
+ }
+ return GridCells(count)
+ }
+
+ val Adaptive = GridCells(0)
+ }
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
new file mode 100644
index 0000000..17a9bb2
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyVerticalGridTranslator.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.glance.appwidget.translators
+
+import android.util.Log
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_MUTABLE
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.content.Intent
+import android.content.Intent.FILL_IN_COMPONENT
+import android.widget.RemoteViews
+import androidx.core.widget.RemoteViewsCompat
+import androidx.glance.appwidget.InsertedViewInfo
+import androidx.glance.appwidget.LayoutType
+import androidx.glance.appwidget.TopLevelLayoutsCount
+import androidx.glance.appwidget.TranslationContext
+import androidx.glance.appwidget.applyModifiers
+import androidx.glance.appwidget.insertView
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGrid
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGridListItem
+import androidx.glance.appwidget.lazy.GridCells
+import androidx.glance.appwidget.lazy.ReservedItemIdRangeEnd
+import androidx.glance.appwidget.translateChild
+import androidx.glance.appwidget.translateComposition
+import androidx.glance.layout.Alignment
+
+/**
+ * Translates an EmittableLazyVerticalGrid and its children to a EmittableLazyList.
+ */
+internal fun RemoteViews.translateEmittableLazyVerticalGrid(
+ translationContext: TranslationContext,
+ element: EmittableLazyVerticalGrid,
+) {
+
+ val viewDef = insertView(translationContext, element.gridCells.toLayout(), element.modifier)
+ translateEmittableLazyVerticalGrid(
+ translationContext,
+ element,
+ viewDef,
+ )
+}
+
+private fun RemoteViews.translateEmittableLazyVerticalGrid(
+ translationContext: TranslationContext,
+ element: EmittableLazyVerticalGrid,
+ viewDef: InsertedViewInfo,
+) {
+ check(!translationContext.isLazyCollectionDescendant) {
+ "Glance does not support nested list views."
+ }
+ setPendingIntentTemplate(
+ viewDef.mainViewId,
+ PendingIntent.getActivity(
+ translationContext.context,
+ 0,
+ Intent(),
+ FILL_IN_COMPONENT or FLAG_MUTABLE or FLAG_UPDATE_CURRENT,
+ )
+ )
+ val items = RemoteViewsCompat.RemoteCollectionItems.Builder().apply {
+ val childContext = translationContext.forLazyCollection(viewDef.mainViewId)
+ element.children.foldIndexed(false) { position, previous, itemEmittable ->
+ itemEmittable as EmittableLazyVerticalGridListItem
+ val itemId = itemEmittable.itemId
+ addItem(
+ itemId,
+ translateComposition(
+ childContext.forLazyViewItem(position, LazyVerticalGridItemStartingViewId),
+ listOf(itemEmittable),
+ translationContext.layoutConfiguration.addLayout(itemEmittable),
+ )
+ )
+ // If the user specifies any explicit ids, we assume the list to be stable
+ previous || (itemId > ReservedItemIdRangeEnd)
+ }.let { setHasStableIds(it) }
+ setViewTypeCount(TopLevelLayoutsCount)
+ }.build()
+ RemoteViewsCompat.setRemoteAdapter(
+ translationContext.context,
+ this,
+ translationContext.appWidgetId,
+ viewDef.mainViewId,
+ items
+ )
+ applyModifiers(translationContext, this, element.modifier, viewDef)
+}
+
+/**
+ * Translates a list item either to its immediate only child, or a column layout wrapping all its
+ * children.
+ */
+internal fun RemoteViews.translateEmittableLazyVerticalGridListItem(
+ translationContext: TranslationContext,
+ element: EmittableLazyVerticalGridListItem
+) {
+ require(element.children.size == 1 && element.alignment == Alignment.CenterStart) {
+ "Lazy vertical grid items can only have a single child align at the center start of the " +
+ "view. The normalization of the composition tree failed."
+ }
+ translateChild(translationContext, element.children.first())
+}
+
+// All the lazy list items should use the same ids, to ensure the layouts can be re-used.
+// Using a very high number to avoid collision with the main app widget ids.
+private const val LazyVerticalGridItemStartingViewId: Int = 0x00100000
+
+private fun GridCells.toLayout(): LayoutType =
+ when (this) {
+ GridCells.Adaptive -> LayoutType.VerticalGridAutoFit
+ GridCells.Fixed(1) -> LayoutType.VerticalGridOneColumn
+ GridCells.Fixed(2) -> LayoutType.VerticalGridTwoColumns
+ GridCells.Fixed(3) -> LayoutType.VerticalGridThreeColumns
+ GridCells.Fixed(4) -> LayoutType.VerticalGridFourColumns
+ GridCells.Fixed(5) -> LayoutType.VerticalGridFiveColumns
+ else -> {
+ Log.w("GlanceLazyVerticalGrid",
+"Unknown GridCells $this number of columns to AutoFit instead")
+ LayoutType.VerticalGridAutoFit
+ }
+ }
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_auto_fit.xml b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_auto_fit.xml
new file mode 100644
index 0000000..bc9f7e45
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_auto_fit.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:divider="@null"
+ android:numColumns="auto_fit"
+ style="@style/Glance.AppWidget.VerticalGrid"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_five_columns.xml b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_five_columns.xml
new file mode 100644
index 0000000..d4eb3d0
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_five_columns.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:divider="@null"
+ android:numColumns="5"
+ style="@style/Glance.AppWidget.VerticalGrid"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_four_columns.xml b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_four_columns.xml
new file mode 100644
index 0000000..c5779a6
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_four_columns.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:divider="@null"
+ android:numColumns="4"
+ style="@style/Glance.AppWidget.VerticalGrid"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_one_column.xml b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_one_column.xml
new file mode 100644
index 0000000..130b429
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_one_column.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:divider="@null"
+ android:numColumns="1"
+ style="@style/Glance.AppWidget.VerticalGrid"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_three_columns.xml b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_three_columns.xml
new file mode 100644
index 0000000..226862ec
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_three_columns.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:divider="@null"
+ android:numColumns="3"
+ style="@style/Glance.AppWidget.VerticalGrid"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_two_columns.xml b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_two_columns.xml
new file mode 100644
index 0000000..5243b80
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/res/layout/vertical_grid_two_columns.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:divider="@null"
+ android:numColumns="2"
+ style="@style/Glance.AppWidget.VerticalGrid"/>
diff --git a/glance/glance-appwidget/src/androidMain/res/values/styles.xml b/glance/glance-appwidget/src/androidMain/res/values/styles.xml
index 5961bec..41718cb 100644
--- a/glance/glance-appwidget/src/androidMain/res/values/styles.xml
+++ b/glance/glance-appwidget/src/androidMain/res/values/styles.xml
@@ -33,6 +33,7 @@
<style name="Glance.AppWidget.LinearProgressIndicator"
parent="@android:style/Widget.ProgressBar.Horizontal"/>
<style name="Glance.AppWidget.CircularProgressIndicator" parent=""/>
+ <style name="Glance.AppWidget.VerticalGrid" parent=""/>
<style name="Base.Glance.AppWidget.Background" parent="">
<item name="android:id">@android:id/background</item>
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
index e0fd8ff..01f8de1 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
@@ -27,6 +27,7 @@
import android.util.Log
import android.view.View
import android.widget.FrameLayout
+import android.widget.GridView
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ListView
@@ -46,6 +47,8 @@
import androidx.glance.appwidget.TextViewSubject.Companion.assertThat
import androidx.glance.appwidget.ViewSubject.Companion.assertThat
import androidx.glance.appwidget.lazy.LazyColumn
+import androidx.glance.appwidget.lazy.LazyVerticalGrid
+import androidx.glance.appwidget.lazy.GridCells
import androidx.glance.appwidget.lazy.ReservedItemIdRangeEnd
import androidx.glance.appwidget.test.R
import androidx.glance.background
@@ -583,6 +586,38 @@
}
@Test
+ fun canTranslateLazyVerticalGrid_emptyList() = fakeCoroutineScope.runBlockingTest {
+ val rv = context.runAndTranslate {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) { }
+ }
+
+ assertIs<GridView>(context.applyRemoteViews(rv))
+ }
+
+ @Test
+ fun canTranslateLazyVerticalGrid_withItem() = fakeCoroutineScope.runBlockingTest {
+ val rv = context.runAndTranslate {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ item { Text("First") }
+ item { Row { Text("Second") } }
+ }
+ }
+
+ assertIs<GridView>(context.applyRemoteViews(rv))
+ }
+
+ @Test
+ fun canTranslateLazyVerticalGrid_withItems() = fakeCoroutineScope.runBlockingTest {
+ val rv = context.runAndTranslate {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ items(2, { it * 2L }) { index -> Text("Item $index") }
+ }
+ }
+
+ assertIs<GridView>(context.applyRemoteViews(rv))
+ }
+
+ @Test
fun canTranslateAndroidRemoteViews() = fakeCoroutineScope.runBlockingTest {
val result = context.runAndTranslate {
val providedViews = RemoteViews(context.packageName, R.layout.text_sample).also {
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyVerticalGridTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyVerticalGridTest.kt
new file mode 100644
index 0000000..6068240
--- /dev/null
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyVerticalGridTest.kt
@@ -0,0 +1,262 @@
+/*
+ * 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.glance.appwidget.layout
+
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGrid
+import androidx.glance.appwidget.lazy.EmittableLazyVerticalGridListItem
+import androidx.glance.appwidget.lazy.LazyVerticalGrid
+import androidx.glance.appwidget.lazy.GridCells
+import androidx.glance.appwidget.lazy.ReservedItemIdRangeEnd
+import androidx.glance.appwidget.lazy.items
+import androidx.glance.appwidget.lazy.itemsIndexed
+import androidx.glance.appwidget.runTestingComposition
+import androidx.glance.layout.EmittableRow
+import androidx.glance.layout.Row
+import androidx.glance.text.EmittableText
+import androidx.glance.text.Text
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import kotlin.test.assertIs
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class LazyVerticalGridTest {
+ private lateinit var fakeCoroutineScope: TestCoroutineScope
+
+ @Before
+ fun setUp() {
+ fakeCoroutineScope = TestCoroutineScope()
+ }
+
+ @Test
+ fun emptyLazyVerticalGrid_addsLazyVerticalGridToTree() = fakeCoroutineScope.runBlockingTest {
+ val root = runTestingComposition {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) { }
+ }
+
+ val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+ assertThat(verticalGrid.children).isEmpty()
+ }
+
+ @Test
+ fun items_createsListItemsEachWithChild() = fakeCoroutineScope.runBlockingTest {
+ val root = runTestingComposition {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ items(2, { it * 2L }) { index -> Text("Item $index") }
+ item(4L) { Text("Item 2") }
+ item(6L) { Text("Item 3") }
+ }
+ }
+
+ val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+ val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+ assertThat(listItems).hasSize(4)
+ assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("Item 0")
+ assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("Item 1")
+ assertThat(verticalGrid.getTextAtChild(2)).isEqualTo("Item 2")
+ assertThat(verticalGrid.getTextAtChild(3)).isEqualTo("Item 3")
+ }
+
+ @Test
+ fun item_multipleChildren_createsListItemWithChildren() = fakeCoroutineScope.runBlockingTest {
+ val root = runTestingComposition {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ item {
+ Text("First")
+ Row { Text("Second") }
+ }
+ }
+ }
+
+ val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+ val listItem = assertIs<EmittableLazyVerticalGridListItem>(verticalGrid.children.single())
+ assertThat(listItem.children).hasSize(2)
+ assertIs<EmittableText>(listItem.children[0])
+ assertIs<EmittableRow>(listItem.children[1])
+ }
+
+ @Test
+ fun items_withItemId_addsChildrenWithIds() = fakeCoroutineScope.runBlockingTest {
+ val root = runTestingComposition {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ items(2, { it * 2L }) { index -> Text("Item $index") }
+ item(4L) { Text("Item 2") }
+ item(6L) { Text("Item 3") }
+ }
+ }
+
+ val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+ val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+ assertThat(listItems[0].itemId).isEqualTo(0L)
+ assertThat(listItems[1].itemId).isEqualTo(2L)
+ assertThat(listItems[2].itemId).isEqualTo(4L)
+ assertThat(listItems[3].itemId).isEqualTo(6L)
+ }
+
+ @Test
+ fun items_withoutItemId_addsItemsWithConsecutiveReservedIds() =
+ fakeCoroutineScope.runBlockingTest {
+ val root = runTestingComposition {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ items(2) { index -> Text("Item $index") }
+ item { Text("Item 2") }
+ }
+ }
+
+ val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+ val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+ assertThat(listItems[0].itemId).isEqualTo(ReservedItemIdRangeEnd)
+ assertThat(listItems[1].itemId).isEqualTo(ReservedItemIdRangeEnd - 1)
+ assertThat(listItems[2].itemId).isEqualTo(ReservedItemIdRangeEnd - 2)
+ }
+
+ @Test
+ fun items_someWithItemIds_addsChildrenWithIds() = fakeCoroutineScope.runBlockingTest {
+ val root = runTestingComposition {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ items(1, { 5L }) { Text("Item 0") }
+ item { Text("Item 1") }
+ items(1) { Text("Item 2") }
+ item(6L) { Text("Item 3") }
+ }
+ }
+
+ val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+ val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+ assertThat(listItems[0].itemId).isEqualTo(5L)
+ assertThat(listItems[1].itemId).isEqualTo(ReservedItemIdRangeEnd)
+ assertThat(listItems[2].itemId).isEqualTo(ReservedItemIdRangeEnd - 1)
+ assertThat(listItems[3].itemId).isEqualTo(6L)
+ }
+
+ @Test
+ fun items_listItemsWithoutItemIds_addsChildren() = fakeCoroutineScope.runBlockingTest {
+ val root = runTestingComposition {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ items(people) { person ->
+ Text(person.name)
+ }
+ }
+ }
+
+ val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+ val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+ assertThat(listItems[0].itemId).isEqualTo(ReservedItemIdRangeEnd)
+ assertThat(listItems[1].itemId).isEqualTo(ReservedItemIdRangeEnd - 1)
+ assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("Alice")
+ assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("Bob")
+ }
+
+ @Test
+ fun items_listItemsWithItemIds_addsChildren() = fakeCoroutineScope.runBlockingTest {
+ val root = runTestingComposition {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ items(people, itemId = { person -> person.userId }) { person ->
+ Text(person.name)
+ }
+ }
+ }
+
+ val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+ val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+ assertThat(listItems[0].itemId).isEqualTo(101)
+ assertThat(listItems[1].itemId).isEqualTo(202)
+ assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("Alice")
+ assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("Bob")
+ }
+
+ @Test
+ fun itemsIndexed_listItems_addsChildren() = fakeCoroutineScope.runBlockingTest {
+ val root = runTestingComposition {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ itemsIndexed(people) { index, person ->
+ Text("${index + 1} - ${person.name}")
+ }
+ }
+ }
+
+ val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+ assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("1 - Alice")
+ assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("2 - Bob")
+ }
+
+ @Test
+ fun items_arrayItemsWithoutItemIds_addsChildren() = fakeCoroutineScope.runBlockingTest {
+ val root = runTestingComposition {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ items(people.toTypedArray()) { person ->
+ Text(person.name)
+ }
+ }
+ }
+
+ val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+ val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+ assertThat(listItems[0].itemId).isEqualTo(ReservedItemIdRangeEnd)
+ assertThat(listItems[1].itemId).isEqualTo(ReservedItemIdRangeEnd - 1)
+ assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("Alice")
+ assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("Bob")
+ }
+
+ @Test
+ fun items_arrayItemsWithItemIds_addsChildren() = fakeCoroutineScope.runBlockingTest {
+ val root = runTestingComposition {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ items(people.toTypedArray(), itemId = { person -> person.userId }) { person ->
+ Text(person.name)
+ }
+ }
+ }
+
+ val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+ val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
+ assertThat(listItems[0].itemId).isEqualTo(101)
+ assertThat(listItems[1].itemId).isEqualTo(202)
+ assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("Alice")
+ assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("Bob")
+ }
+
+ @Test
+ fun itemsIndexed_arrayItems_addsChildren() = fakeCoroutineScope.runBlockingTest {
+ val root = runTestingComposition {
+ LazyVerticalGrid(gridCells = GridCells.Adaptive) {
+ itemsIndexed(people.toTypedArray()) { index, person ->
+ Text("${index + 1} - ${person.name}")
+ }
+ }
+ }
+
+ val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
+ assertThat(verticalGrid.getTextAtChild(0)).isEqualTo("1 - Alice")
+ assertThat(verticalGrid.getTextAtChild(1)).isEqualTo("2 - Bob")
+ }
+
+ private fun EmittableLazyVerticalGrid.getTextAtChild(index: Int): String =
+ assertIs<EmittableText>((children[index] as
+ EmittableLazyVerticalGridListItem).children.first()).text
+
+ private companion object {
+ data class Person(val name: String, val userId: Long)
+ val people = listOf(Person("Alice", userId = 101), Person("Bob", userId = 202))
+ }
+}
+
+private inline fun <reified T> assertAre(items: Iterable<Any>) =
+ items.map { assertIs<T>(it) }.toList()
diff --git a/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt b/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt
index d7b8a69..a0ce679 100644
--- a/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt
+++ b/lifecycle/integration-tests/kotlintestapp/src/test-common/java/androidx.lifecycle/LifecycleCoroutineScopeTestBase.kt
@@ -59,7 +59,8 @@
@Test
fun launchAfterDestroy() {
- val owner = TestLifecycleOwner(Lifecycle.State.DESTROYED, TestCoroutineDispatcher())
+ val owner = TestLifecycleOwner(Lifecycle.State.CREATED, TestCoroutineDispatcher())
+ owner.lifecycle.currentState = Lifecycle.State.DESTROYED
runBlocking {
owner.lifecycleScope.launch {
// do nothing
diff --git a/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java b/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
index 77e06a7..fcdd42b 100644
--- a/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
+++ b/lifecycle/lifecycle-livedata-core/src/test/java/androidx/lifecycle/LiveDataTest.java
@@ -233,6 +233,7 @@
@Test
public void testInactiveRegistry() {
Observer<String> observer = (Observer<String>) mock(Observer.class);
+ mOwner.handleLifecycleEvent(ON_CREATE);
mOwner.handleLifecycleEvent(ON_DESTROY);
mLiveData.observe(mOwner, observer);
assertThat(mLiveData.hasObservers(), is(false));
diff --git a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FlowWithLifecycleTest.kt b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FlowWithLifecycleTest.kt
index d6e6ed3..7e6f298 100644
--- a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FlowWithLifecycleTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/FlowWithLifecycleTest.kt
@@ -65,6 +65,7 @@
@Test
fun testFlowDoesNotCollectIfLifecycleIsDestroyed() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.CREATED)
owner.setState(Lifecycle.State.DESTROYED)
val result = flowOf(1, 2, 3)
.flowWithLifecycle(owner.lifecycle, Lifecycle.State.RESUMED)
diff --git a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt
index c4ff4be..6d64b64 100644
--- a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/RepeatOnLifecycleTest.kt
@@ -202,6 +202,7 @@
@Test
fun testBlockDoesNotStartIfLifecycleIsDestroyed() = runBlocking(Dispatchers.Main) {
+ owner.setState(Lifecycle.State.CREATED)
owner.setState(Lifecycle.State.DESTROYED)
expectations.expect(1)
diff --git a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/WithLifecycleStateTest.kt b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/WithLifecycleStateTest.kt
index 75c2e55..66f4780 100644
--- a/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/WithLifecycleStateTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx/src/androidTest/java/androidx/lifecycle/WithLifecycleStateTest.kt
@@ -58,7 +58,8 @@
@Test
fun testBlockCancelledWhenInitiallyDestroyed() = runBlocking(Dispatchers.Main) {
- val owner = FakeLifecycleOwner(Lifecycle.State.DESTROYED)
+ val owner = FakeLifecycleOwner(Lifecycle.State.CREATED)
+ owner.setState(Lifecycle.State.DESTROYED)
val result = runCatching {
owner.withStarted {}
diff --git a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java
index 03d96b7..a4a6ed4 100644
--- a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java
+++ b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java
@@ -138,6 +138,9 @@
if (mState == next) {
return;
}
+ if (mState == INITIALIZED && next == DESTROYED) {
+ throw new IllegalStateException("no event down from " + mState);
+ }
mState = next;
if (mHandlingEvent || mAddingObserverCounter != 0) {
mNewEventOccurred = true;
@@ -147,6 +150,9 @@
mHandlingEvent = true;
sync();
mHandlingEvent = false;
+ if (mState == DESTROYED) {
+ mObserverMap = new FastSafeIterableMap<>();
+ }
}
private boolean isSynced() {
diff --git a/lifecycle/lifecycle-runtime/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java b/lifecycle/lifecycle-runtime/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java
index 3130d48..68de883 100644
--- a/lifecycle/lifecycle-runtime/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java
+++ b/lifecycle/lifecycle-runtime/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java
@@ -64,6 +64,15 @@
}
@Test
+ public void moveInitializedToDestroyed() {
+ try {
+ mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+ } catch (IllegalStateException e) {
+ assertThat(e.getMessage(), is("no event down from INITIALIZED"));
+ }
+ }
+
+ @Test
public void setCurrentState() {
mRegistry.setCurrentState(Lifecycle.State.RESUMED);
assertThat(mRegistry.getCurrentState(), is(Lifecycle.State.RESUMED));
diff --git a/room/room-common/api/current.txt b/room/room-common/api/current.txt
index 7df459b..85ffce4 100644
--- a/room/room-common/api/current.txt
+++ b/room/room-common/api/current.txt
@@ -24,16 +24,16 @@
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ColumnInfo {
- method public abstract int collate() default androidx.room.ColumnInfo.UNSPECIFIED;
+ method @androidx.room.ColumnInfo.Collate public abstract int collate() default androidx.room.ColumnInfo.UNSPECIFIED;
method public abstract String defaultValue() default androidx.room.ColumnInfo.VALUE_UNSPECIFIED;
method public abstract boolean index() default false;
method public abstract String name() default androidx.room.ColumnInfo.INHERIT_FIELD_NAME;
- method public abstract int typeAffinity() default androidx.room.ColumnInfo.UNDEFINED;
- property public abstract int collate;
+ method @androidx.room.ColumnInfo.SQLiteTypeAffinity public abstract int typeAffinity() default androidx.room.ColumnInfo.UNDEFINED;
+ property @androidx.room.ColumnInfo.Collate public abstract int collate;
property public abstract String defaultValue;
property public abstract boolean index;
property public abstract String name;
- property public abstract int typeAffinity;
+ property @androidx.room.ColumnInfo.SQLiteTypeAffinity public abstract int typeAffinity;
field public static final int BINARY = 2; // 0x2
field public static final int BLOB = 5; // 0x5
field public static final androidx.room.ColumnInfo.Companion Companion;
@@ -146,14 +146,14 @@
method public abstract String[] childColumns();
method public abstract boolean deferred() default false;
method public abstract kotlin.reflect.KClass<?> entity();
- method public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
- method public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
+ method @androidx.room.ForeignKey.Action public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
+ method @androidx.room.ForeignKey.Action public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
method public abstract String[] parentColumns();
property public abstract String![] childColumns;
property public abstract boolean deferred;
property public abstract kotlin.reflect.KClass<?> entity;
- property public abstract int onDelete;
- property public abstract int onUpdate;
+ property @androidx.room.ForeignKey.Action public abstract int onDelete;
+ property @androidx.room.ForeignKey.Action public abstract int onUpdate;
property public abstract String![] parentColumns;
field public static final int CASCADE = 5; // 0x5
field public static final androidx.room.ForeignKey.Companion Companion;
@@ -239,9 +239,9 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Insert {
method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
- method public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
+ method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
property public abstract kotlin.reflect.KClass<?> entity;
- property public abstract int onConflict;
+ property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={}) public @interface Junction {
@@ -260,7 +260,7 @@
property public abstract String valueColumn;
}
- @IntDef({androidx.room.OnConflictStrategy.REPLACE, androidx.room.OnConflictStrategy.ROLLBACK, androidx.room.OnConflictStrategy.ABORT, androidx.room.OnConflictStrategy.FAIL, androidx.room.OnConflictStrategy.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
+ @IntDef({androidx.room.OnConflictStrategy.Companion.REPLACE, androidx.room.OnConflictStrategy.Companion.ROLLBACK, androidx.room.OnConflictStrategy.Companion.ABORT, androidx.room.OnConflictStrategy.Companion.FAIL, androidx.room.OnConflictStrategy.Companion.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
field public static final int ABORT = 3; // 0x3
field public static final androidx.room.OnConflictStrategy.Companion Companion;
field @Deprecated public static final int FAIL = 4; // 0x4
@@ -383,9 +383,9 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Update {
method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
- method public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
+ method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
property public abstract kotlin.reflect.KClass<?> entity;
- property public abstract int onConflict;
+ property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
}
diff --git a/room/room-common/api/public_plus_experimental_current.txt b/room/room-common/api/public_plus_experimental_current.txt
index 7df459b..85ffce4 100644
--- a/room/room-common/api/public_plus_experimental_current.txt
+++ b/room/room-common/api/public_plus_experimental_current.txt
@@ -24,16 +24,16 @@
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ColumnInfo {
- method public abstract int collate() default androidx.room.ColumnInfo.UNSPECIFIED;
+ method @androidx.room.ColumnInfo.Collate public abstract int collate() default androidx.room.ColumnInfo.UNSPECIFIED;
method public abstract String defaultValue() default androidx.room.ColumnInfo.VALUE_UNSPECIFIED;
method public abstract boolean index() default false;
method public abstract String name() default androidx.room.ColumnInfo.INHERIT_FIELD_NAME;
- method public abstract int typeAffinity() default androidx.room.ColumnInfo.UNDEFINED;
- property public abstract int collate;
+ method @androidx.room.ColumnInfo.SQLiteTypeAffinity public abstract int typeAffinity() default androidx.room.ColumnInfo.UNDEFINED;
+ property @androidx.room.ColumnInfo.Collate public abstract int collate;
property public abstract String defaultValue;
property public abstract boolean index;
property public abstract String name;
- property public abstract int typeAffinity;
+ property @androidx.room.ColumnInfo.SQLiteTypeAffinity public abstract int typeAffinity;
field public static final int BINARY = 2; // 0x2
field public static final int BLOB = 5; // 0x5
field public static final androidx.room.ColumnInfo.Companion Companion;
@@ -146,14 +146,14 @@
method public abstract String[] childColumns();
method public abstract boolean deferred() default false;
method public abstract kotlin.reflect.KClass<?> entity();
- method public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
- method public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
+ method @androidx.room.ForeignKey.Action public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
+ method @androidx.room.ForeignKey.Action public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
method public abstract String[] parentColumns();
property public abstract String![] childColumns;
property public abstract boolean deferred;
property public abstract kotlin.reflect.KClass<?> entity;
- property public abstract int onDelete;
- property public abstract int onUpdate;
+ property @androidx.room.ForeignKey.Action public abstract int onDelete;
+ property @androidx.room.ForeignKey.Action public abstract int onUpdate;
property public abstract String![] parentColumns;
field public static final int CASCADE = 5; // 0x5
field public static final androidx.room.ForeignKey.Companion Companion;
@@ -239,9 +239,9 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Insert {
method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
- method public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
+ method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
property public abstract kotlin.reflect.KClass<?> entity;
- property public abstract int onConflict;
+ property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={}) public @interface Junction {
@@ -260,7 +260,7 @@
property public abstract String valueColumn;
}
- @IntDef({androidx.room.OnConflictStrategy.REPLACE, androidx.room.OnConflictStrategy.ROLLBACK, androidx.room.OnConflictStrategy.ABORT, androidx.room.OnConflictStrategy.FAIL, androidx.room.OnConflictStrategy.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
+ @IntDef({androidx.room.OnConflictStrategy.Companion.REPLACE, androidx.room.OnConflictStrategy.Companion.ROLLBACK, androidx.room.OnConflictStrategy.Companion.ABORT, androidx.room.OnConflictStrategy.Companion.FAIL, androidx.room.OnConflictStrategy.Companion.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
field public static final int ABORT = 3; // 0x3
field public static final androidx.room.OnConflictStrategy.Companion Companion;
field @Deprecated public static final int FAIL = 4; // 0x4
@@ -383,9 +383,9 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Update {
method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
- method public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
+ method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
property public abstract kotlin.reflect.KClass<?> entity;
- property public abstract int onConflict;
+ property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
}
diff --git a/room/room-common/api/restricted_current.txt b/room/room-common/api/restricted_current.txt
index 8149ffa..f9cc4615 100644
--- a/room/room-common/api/restricted_current.txt
+++ b/room/room-common/api/restricted_current.txt
@@ -24,16 +24,16 @@
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ColumnInfo {
- method public abstract int collate() default androidx.room.ColumnInfo.UNSPECIFIED;
+ method @androidx.room.ColumnInfo.Collate public abstract int collate() default androidx.room.ColumnInfo.UNSPECIFIED;
method public abstract String defaultValue() default androidx.room.ColumnInfo.VALUE_UNSPECIFIED;
method public abstract boolean index() default false;
method public abstract String name() default androidx.room.ColumnInfo.INHERIT_FIELD_NAME;
- method public abstract int typeAffinity() default androidx.room.ColumnInfo.UNDEFINED;
- property public abstract int collate;
+ method @androidx.room.ColumnInfo.SQLiteTypeAffinity public abstract int typeAffinity() default androidx.room.ColumnInfo.UNDEFINED;
+ property @androidx.room.ColumnInfo.Collate public abstract int collate;
property public abstract String defaultValue;
property public abstract boolean index;
property public abstract String name;
- property public abstract int typeAffinity;
+ property @androidx.room.ColumnInfo.SQLiteTypeAffinity public abstract int typeAffinity;
field public static final int BINARY = 2; // 0x2
field public static final int BLOB = 5; // 0x5
field public static final androidx.room.ColumnInfo.Companion Companion;
@@ -146,14 +146,14 @@
method public abstract String[] childColumns();
method public abstract boolean deferred() default false;
method public abstract kotlin.reflect.KClass<?> entity();
- method public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
- method public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
+ method @androidx.room.ForeignKey.Action public abstract int onDelete() default androidx.room.ForeignKey.NO_ACTION;
+ method @androidx.room.ForeignKey.Action public abstract int onUpdate() default androidx.room.ForeignKey.NO_ACTION;
method public abstract String[] parentColumns();
property public abstract String![] childColumns;
property public abstract boolean deferred;
property public abstract kotlin.reflect.KClass<?> entity;
- property public abstract int onDelete;
- property public abstract int onUpdate;
+ property @androidx.room.ForeignKey.Action public abstract int onDelete;
+ property @androidx.room.ForeignKey.Action public abstract int onUpdate;
property public abstract String![] parentColumns;
field public static final int CASCADE = 5; // 0x5
field public static final androidx.room.ForeignKey.Companion Companion;
@@ -239,9 +239,9 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Insert {
method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
- method public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
+ method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
property public abstract kotlin.reflect.KClass<?> entity;
- property public abstract int onConflict;
+ property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={}) public @interface Junction {
@@ -260,7 +260,7 @@
property public abstract String valueColumn;
}
- @IntDef({androidx.room.OnConflictStrategy.REPLACE, androidx.room.OnConflictStrategy.ROLLBACK, androidx.room.OnConflictStrategy.ABORT, androidx.room.OnConflictStrategy.FAIL, androidx.room.OnConflictStrategy.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
+ @IntDef({androidx.room.OnConflictStrategy.Companion.REPLACE, androidx.room.OnConflictStrategy.Companion.ROLLBACK, androidx.room.OnConflictStrategy.Companion.ABORT, androidx.room.OnConflictStrategy.Companion.FAIL, androidx.room.OnConflictStrategy.Companion.IGNORE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface OnConflictStrategy {
field public static final int ABORT = 3; // 0x3
field public static final androidx.room.OnConflictStrategy.Companion Companion;
field @Deprecated public static final int FAIL = 4; // 0x4
@@ -392,9 +392,9 @@
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface Update {
method public abstract kotlin.reflect.KClass<?> entity() default java.lang.Object;
- method public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
+ method @androidx.room.OnConflictStrategy public abstract int onConflict() default androidx.room.OnConflictStrategy.ABORT;
property public abstract kotlin.reflect.KClass<?> entity;
- property public abstract int onConflict;
+ property @androidx.room.OnConflictStrategy public abstract int onConflict;
}
}
diff --git a/room/room-common/src/main/java/androidx/room/ColumnInfo.kt b/room/room-common/src/main/java/androidx/room/ColumnInfo.kt
index c3817a0d..9134c14 100644
--- a/room/room-common/src/main/java/androidx/room/ColumnInfo.kt
+++ b/room/room-common/src/main/java/androidx/room/ColumnInfo.kt
@@ -46,7 +46,7 @@
* [INTEGER], [REAL], or [BLOB].
*/
@SuppressWarnings("unused")
- @SQLiteTypeAffinity
+ @get:SQLiteTypeAffinity
val typeAffinity: Int = UNDEFINED,
/**
@@ -67,7 +67,7 @@
* @return The collation sequence of the column. This is either [UNSPECIFIED],
* [BINARY], [NOCASE], [RTRIM], [LOCALIZED] or [UNICODE].
*/
- @Collate
+ @get:Collate
val collate: Int = UNSPECIFIED,
/**
diff --git a/room/room-common/src/main/java/androidx/room/ForeignKey.kt b/room/room-common/src/main/java/androidx/room/ForeignKey.kt
index 38f9b76..35f4f99 100644
--- a/room/room-common/src/main/java/androidx/room/ForeignKey.kt
+++ b/room/room-common/src/main/java/androidx/room/ForeignKey.kt
@@ -80,7 +80,7 @@
*
* @return The action to take when the referenced entity is deleted from the database.
*/
- @Action
+ @get:Action
val onDelete: Int = NO_ACTION,
/**
@@ -90,7 +90,7 @@
*
* @return The action to take when the referenced entity is updated in the database.
*/
- @Action
+ @get:Action
val onUpdate: Int = NO_ACTION,
/**
diff --git a/room/room-common/src/main/java/androidx/room/Insert.kt b/room/room-common/src/main/java/androidx/room/Insert.kt
index 8fe2cf7..cc9acdc 100644
--- a/room/room-common/src/main/java/androidx/room/Insert.kt
+++ b/room/room-common/src/main/java/androidx/room/Insert.kt
@@ -109,6 +109,6 @@
*
* @return How to handle conflicts. Defaults to [OnConflictStrategy.ABORT].
*/
- @OnConflictStrategy
+ @get:OnConflictStrategy
val onConflict: Int = OnConflictStrategy.ABORT
)
diff --git a/room/room-common/src/main/java/androidx/room/OnConflictStrategy.kt b/room/room-common/src/main/java/androidx/room/OnConflictStrategy.kt
index dcce625..cb4b260d 100644
--- a/room/room-common/src/main/java/androidx/room/OnConflictStrategy.kt
+++ b/room/room-common/src/main/java/androidx/room/OnConflictStrategy.kt
@@ -16,18 +16,19 @@
package androidx.room
import androidx.annotation.IntDef
-import androidx.room.OnConflictStrategy.Companion.ABORT
-import androidx.room.OnConflictStrategy.Companion.FAIL
-import androidx.room.OnConflictStrategy.Companion.IGNORE
-import androidx.room.OnConflictStrategy.Companion.REPLACE
-import androidx.room.OnConflictStrategy.Companion.ROLLBACK
/**
* Set of conflict handling strategies for various {@link Dao} methods.
*/
@Retention(AnnotationRetention.BINARY)
@Suppress("DEPRECATION")
-@IntDef(REPLACE, ROLLBACK, ABORT, FAIL, IGNORE)
+@IntDef(
+ OnConflictStrategy.REPLACE,
+ OnConflictStrategy.ROLLBACK,
+ OnConflictStrategy.ABORT,
+ OnConflictStrategy.FAIL,
+ OnConflictStrategy.IGNORE
+)
public annotation class OnConflictStrategy {
public companion object {
/**
diff --git a/room/room-common/src/main/java/androidx/room/Update.kt b/room/room-common/src/main/java/androidx/room/Update.kt
index b60a2ab..2e8793f 100644
--- a/room/room-common/src/main/java/androidx/room/Update.kt
+++ b/room/room-common/src/main/java/androidx/room/Update.kt
@@ -105,6 +105,6 @@
*
* @return How to handle conflicts. Defaults to [OnConflictStrategy.ABORT].
*/
- @OnConflictStrategy
+ @get:OnConflictStrategy
val onConflict: Int = OnConflictStrategy.ABORT
)
diff --git a/settings.gradle b/settings.gradle
index e2907f9..6f2f3c3 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -298,6 +298,7 @@
includeProject(":camera:integration-tests:camera-testapp-camera2-pipe", "camera/integration-tests/camerapipetestapp", [BuildType.MAIN])
includeProject(":camera:integration-tests:camera-testapp-core", "camera/integration-tests/coretestapp", [BuildType.MAIN])
includeProject(":camera:integration-tests:camera-testapp-extensions", "camera/integration-tests/extensionstestapp", [BuildType.MAIN])
+includeProject(":camera:integration-tests:camera-testapp-previewview", "camera/integration-tests/previewviewtestapp", [BuildType.MAIN])
includeProject(":camera:integration-tests:camera-testapp-timing", "camera/integration-tests/timingtestapp", [BuildType.MAIN])
includeProject(":camera:integration-tests:camera-testapp-uiwidgets", "camera/integration-tests/uiwidgetstestapp", [BuildType.MAIN])
includeProject(":camera:integration-tests:camera-testapp-view", "camera/integration-tests/viewtestapp", [BuildType.MAIN])
diff --git a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedRowTest.kt b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedRowTest.kt
index 52f3c30..4d02d12 100644
--- a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedRowTest.kt
+++ b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedRowTest.kt
@@ -1,3 +1,21 @@
+/*
+ * Copyright 2021 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.wear.compose.foundation
+
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.ui.Modifier
@@ -6,16 +24,13 @@
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.dp
-import androidx.wear.compose.foundation.AnchorType
-import androidx.wear.compose.foundation.CurvedRow
-import androidx.wear.compose.foundation.RadialAlignment
+import kotlin.math.PI
+import kotlin.math.atan2
+import kotlin.math.min
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Rule
import org.junit.Test
-import kotlin.math.PI
-import kotlin.math.atan2
-import kotlin.math.min
// When components are laid out, position is specified by integers, so we can't expect
// much precision.
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeToDismissBoxTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeToDismissBoxTest.kt
index b8de966..05982c7 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeToDismissBoxTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeToDismissBoxTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+package androidx.wear.compose.material
+
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -40,16 +42,6 @@
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
-import androidx.wear.compose.material.Button
-import androidx.wear.compose.material.ExperimentalWearMaterialApi
-import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.SwipeDismissTarget
-import androidx.wear.compose.material.SwipeToDismissBox
-import androidx.wear.compose.material.TEST_TAG
-import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.ToggleButton
-import androidx.wear.compose.material.rememberSwipeToDismissBoxState
-import androidx.wear.compose.material.setContentWithTheme
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -317,10 +309,10 @@
}
}
-internal const val BACKGROUND_MESSAGE = "The Background"
-internal const val CONTENT_MESSAGE = "The Content"
-internal const val LONG_SWIPE = 1000L
-internal const val TOGGLE_SCREEN = "Toggle"
-internal const val COUNTER_SCREEN = "Counter"
-internal const val TOGGLE_ON = "On"
-internal const val TOGGLE_OFF = "Off"
\ No newline at end of file
+private const val BACKGROUND_MESSAGE = "The Background"
+private const val CONTENT_MESSAGE = "The Content"
+private const val LONG_SWIPE = 1000L
+private const val TOGGLE_SCREEN = "Toggle"
+private const val COUNTER_SCREEN = "Counter"
+private const val TOGGLE_ON = "On"
+private const val TOGGLE_OFF = "Off"
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeableTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeableTest.kt
index 05b8c1a..21427f7 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeableTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/SwipeableTest.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+package androidx.wear.compose.material
+
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.Orientation.Horizontal
import androidx.compose.foundation.gestures.Orientation.Vertical
@@ -38,12 +40,9 @@
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material.ExperimentalWearMaterialApi
-import androidx.wear.compose.material.SwipeableState
-import androidx.wear.compose.material.swipeable
+import kotlin.math.absoluteValue
import org.junit.Rule
import org.junit.Test
-import kotlin.math.absoluteValue
// TODO(b/201009199) Some of these tests may need specific values adjusted when swipeable
// supports property nested scrolling, but the tests should all still be valid.
@@ -228,5 +227,3 @@
} ?: false
}
}
-
-private const val TEST_TAG = "swipeable"