diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AbsVKApiInterceptor.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AbsVKApiInterceptor.kt index 3de269fb0..32533cdc4 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AbsVKApiInterceptor.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/AbsVKApiInterceptor.kt @@ -88,4 +88,4 @@ abstract class AbsVKApiInterceptor(private val version: String) : .build() ) } -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CustomTokenVKApiInterceptor.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CustomTokenVKApiInterceptor.kt index 1f4afac37..61f204aed 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CustomTokenVKApiInterceptor.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/CustomTokenVKApiInterceptor.kt @@ -25,4 +25,4 @@ internal class CustomTokenVKApiInterceptor( return accountType } override val accountId: Long = account_id ?: -1 -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/DefaultVKApiInterceptor.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/DefaultVKApiInterceptor.kt index 2b8338974..6cc51c604 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/DefaultVKApiInterceptor.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/DefaultVKApiInterceptor.kt @@ -24,4 +24,4 @@ class DefaultVKApiInterceptor internal constructor( get() = Settings.get() .accounts() .getType(accountId) -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IOtherVKRestProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IOtherVKRestProvider.kt index 4e3d97e5b..b9abb8678 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IOtherVKRestProvider.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IOtherVKRestProvider.kt @@ -13,4 +13,4 @@ interface IOtherVKRestProvider { fun provideAuthServiceRest(): Single fun provideLongpollRest(): Single fun provideLocalServerRest(): Single -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVKMethodHttpClientFactory.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVKMethodHttpClientFactory.kt index 4e88bc6ee..d41fdc04d 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVKMethodHttpClientFactory.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVKMethodHttpClientFactory.kt @@ -18,4 +18,4 @@ interface IVKMethodHttpClientFactory { customDeviceName: String?, config: ProxyConfig? ): OkHttpClient.Builder -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVKRestProvider.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVKRestProvider.kt index 36089a6a3..a867051d1 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVKRestProvider.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/IVKRestProvider.kt @@ -14,4 +14,4 @@ interface IVKRestProvider { @AccountType type: Int, customDeviceName: String? ): Single -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/impl/VKApies.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/impl/VKApies.kt index e361d50f3..49dd042f8 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/impl/VKApies.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/impl/VKApies.kt @@ -233,4 +233,4 @@ internal class VKApies private constructor( wallApi = WallApi(accountId, restProvider) otherApi = OtherApi(accountId, provider) } -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/longpoll/VKApiGroupLongpollUpdates.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/longpoll/VKApiGroupLongpollUpdates.kt index b3f3088cb..186ce967c 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/longpoll/VKApiGroupLongpollUpdates.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/longpoll/VKApiGroupLongpollUpdates.kt @@ -12,4 +12,4 @@ class VKApiGroupLongpollUpdates { var ts: String? = null val count: Int get() = 0 -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/longpoll/VKApiLongpollUpdates.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/longpoll/VKApiLongpollUpdates.kt index 0d20f6d81..8283fa9b7 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/longpoll/VKApiLongpollUpdates.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/longpoll/VKApiLongpollUpdates.kt @@ -95,4 +95,4 @@ class VKApiLongpollUpdates { } } } -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/response/VKResponse.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/response/VKResponse.kt index 42763854e..759440ccd 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/response/VKResponse.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/api/model/response/VKResponse.kt @@ -13,4 +13,4 @@ open class VKResponse { @SerialName("execute_errors") var executeErrors: List? = null */ -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotoalbums/VKPhotoAlbumsAdapter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotoalbums/VKPhotoAlbumsAdapter.kt index a2e393465..df69cbeee 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotoalbums/VKPhotoAlbumsAdapter.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotoalbums/VKPhotoAlbumsAdapter.kt @@ -73,4 +73,4 @@ class VKPhotoAlbumsAdapter(private val context: Context, private var data: List< val title: TextView = itemView.findViewById(R.id.item_local_album_name) val counterText: TextView = itemView.findViewById(R.id.counter) } -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotos/BigVKPhotosAdapter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotos/BigVKPhotosAdapter.kt index c7b029a66..ad84e4702 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotos/BigVKPhotosAdapter.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotos/BigVKPhotosAdapter.kt @@ -293,4 +293,4 @@ class BigVKPhotosAdapter( setData(DATA_TYPE_UPLOAD, uploads) setData(DATA_TYPE_PHOTO, photoWrappers) } -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotos/IVKPhotosView.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotos/IVKPhotosView.kt index 4b2dc1ebf..d881986bf 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotos/IVKPhotosView.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotos/IVKPhotosView.kt @@ -50,4 +50,4 @@ interface IVKPhotosView : IMvpView, IErrorView, IToolbarView { const val ACTION_SHOW_PHOTOS = "dev.ragnarok.fenrir.ACTION_SHOW_PHOTOS" const val ACTION_SELECT_PHOTOS = "dev.ragnarok.fenrir.ACTION_SELECT_PHOTOS" } -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotos/VKPhotosPresenter.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotos/VKPhotosPresenter.kt index 4cff4cb54..2880c6c94 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotos/VKPhotosPresenter.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/fragment/vkphotos/VKPhotosPresenter.kt @@ -603,4 +603,4 @@ class VKPhotosPresenter( refreshOwnerInfoIfNeed() refreshAlbumInfoIfNeed() } -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/link/VKLinkParser.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/link/VKLinkParser.kt index 585c4c0bd..f2e0da8ab 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/link/VKLinkParser.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/link/VKLinkParser.kt @@ -750,4 +750,4 @@ object VKLinkParser { @Throws(Exception::class) fun parse(string: String?): Optional } -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/model/selection/VKPhotosSelectableSource.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/model/selection/VKPhotosSelectableSource.kt index 6f12188ec..dc4ec2e4e 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/model/selection/VKPhotosSelectableSource.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/model/selection/VKPhotosSelectableSource.kt @@ -40,4 +40,4 @@ class VKPhotosSelectableSource : AbsSelectableSource { return arrayOfNulls(size) } } -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/VKPlace.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/VKPlace.kt index 39335f62d..a71548c86 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/VKPlace.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/push/VKPlace.kt @@ -110,4 +110,4 @@ open class VKPlace { return null } } -} +} \ No newline at end of file diff --git a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/settings/VKPushRegistration.kt b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/settings/VKPushRegistration.kt index 2b55f3585..948ed7c3b 100644 --- a/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/settings/VKPushRegistration.kt +++ b/app_fenrir/src/main/kotlin/dev/ragnarok/fenrir/settings/VKPushRegistration.kt @@ -33,4 +33,4 @@ class VKPushRegistration { this.gmcToken = gmcToken return this } -} +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 98960168c..86a9e0f56 100644 --- a/build.gradle +++ b/build.gradle @@ -13,11 +13,11 @@ buildscript { ext.appFileGalleryVersionName = "1.999" //androidx libraries - ext.activityVersion = "1.7.0-alpha04" - ext.annotationVersion = "1.6.0-beta01" - ext.appcompatVersion = "1.6.0" + ext.activityVersion = "1.7.0-beta01" + ext.annotationVersion = "1.6.0-rc01" + ext.appcompatVersion = "1.6.1" ext.biometricVersion = "1.2.0-alpha05" - ext.browserVersion = "1.5.0-rc01" + ext.browserVersion = "1.5.0" ext.cameraVersion = "1.2.1" ext.cardviewVersion = "1.0.0" ext.collectionVersion = "1.3.0-alpha02" @@ -27,20 +27,20 @@ buildscript { ext.customviewVersion = "1.2.0-alpha02" ext.customviewPoolingcontainerVersion = "1.0.0" ext.dynamicanimationVersion = "1.1.0-alpha03" - ext.drawerlayoutVersion = "1.2.0-alpha01" - ext.exifinterfaceVersion = "1.3.5" - ext.fragmentVersion = "1.6.0-alpha04" - ext.lifecycleVersion = "2.6.0-alpha05" + ext.drawerlayoutVersion = "1.2.0-beta01" + ext.exifinterfaceVersion = "1.3.6" + ext.fragmentVersion = "1.6.0-alpha05" + ext.lifecycleVersion = "2.6.0-beta01" ext.loaderVersion = "1.1.0" - ext.mediaVersion = "1.6.0" - ext.recyclerviewVersion = "1.3.0-beta02" + ext.mediaVersion = "1.7.0-alpha01" + ext.recyclerviewVersion = "1.3.0-rc01" ext.savedStateVersion = "1.2.0" ext.swiperefreshlayoutVersion = "1.2.0-alpha01" ext.tracingVersion = "1.1.0" ext.transitionVersion = "1.4.1" ext.vectordrawableVersion = "1.2.0-beta01" ext.webkitVersion = "1.6.0" - ext.workVersion = "2.8.0-rc01" + ext.workVersion = "2.8.0" //firebase libraries ext.firebaseDatatransportVersion = "18.1.7" diff --git a/firebase-installations/build.gradle b/firebase-installations/build.gradle index 13435decf..d56d093e7 100644 --- a/firebase-installations/build.gradle +++ b/firebase-installations/build.gradle @@ -1,5 +1,6 @@ plugins { id("com.android.library") + id("kotlin-android") } android { @@ -47,6 +48,9 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 encoding = "utf-8" } + kotlinOptions { + jvmTarget = "1.8" + } } static def asStringVar(String str) { @@ -54,6 +58,10 @@ static def asStringVar(String str) { } dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") + implementation("org.jetbrains.kotlin:kotlin-parcelize-runtime:$kotlin_version") + implementation("org.jetbrains.kotlin:kotlin-android-extensions-runtime:$kotlin_version") + compileOnly("org.jetbrains.kotlin:kotlin-annotations-jvm:$kotlin_version") implementation("com.google.firebase:firebase-common:$firebaseCommonVersion") implementation("com.google.firebase:firebase-installations-interop:$firebaseInstallationsInteropVersion") implementation("com.google.firebase:firebase-components:$firebaseComponentsVersion") diff --git a/image/build.gradle b/image/build.gradle index 59d341e20..936cba551 100644 --- a/image/build.gradle +++ b/image/build.gradle @@ -1,5 +1,6 @@ plugins { id("com.android.library") + id("kotlin-android") } android { @@ -18,17 +19,24 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 encoding = "utf-8" } + kotlinOptions { + jvmTarget = "1.8" + } } dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") + implementation("org.jetbrains.kotlin:kotlin-parcelize-runtime:$kotlin_version") + implementation("org.jetbrains.kotlin:kotlin-android-extensions-runtime:$kotlin_version") + compileOnly("org.jetbrains.kotlin:kotlin-annotations-jvm:$kotlin_version") implementation("androidx.annotation:annotation:$annotationVersion") implementation("androidx.appcompat:appcompat:$appcompatVersion") //implementation("androidx.recyclerview:recyclerview:$recyclerviewVersion") implementation project(path: ":viewpager2") - implementation("androidx.core:core:$coreVersion") - implementation("androidx.activity:activity:$activityVersion") - implementation("androidx.fragment:fragment:$fragmentVersion") + implementation("androidx.core:core-ktx:$coreVersion") + implementation("androidx.activity:activity-ktx:$activityVersion") + implementation("androidx.fragment:fragment-ktx:$fragmentVersion") implementation project(path: ":material") implementation("androidx.constraintlayout:constraintlayout:$constraintlayoutVersion") implementation("io.reactivex.rxjava3:rxjava:$rxJavaVersion") diff --git a/libfenrir/src/main/jni/thorvg/src/lib/sw_engine/tvgSwRenderer.cpp b/libfenrir/src/main/jni/thorvg/src/lib/sw_engine/tvgSwRenderer.cpp index 17dff5727..6a1a88193 100644 --- a/libfenrir/src/main/jni/thorvg/src/lib/sw_engine/tvgSwRenderer.cpp +++ b/libfenrir/src/main/jni/thorvg/src/lib/sw_engine/tvgSwRenderer.cpp @@ -296,7 +296,7 @@ bool SwRenderer::viewport(const RenderRegion& vp) } -bool SwRenderer::target(uint32_t* buffer, uint32_t stride, uint32_t w, uint32_t h, uint32_t cs) +bool SwRenderer::target(uint32_t* buffer, uint32_t stride, uint32_t w, uint32_t h, uint32_t colorSpace) { if (!buffer || stride == 0 || w == 0 || h == 0 || w > stride) return false; @@ -306,7 +306,7 @@ bool SwRenderer::target(uint32_t* buffer, uint32_t stride, uint32_t w, uint32_t surface->stride = stride; surface->w = w; surface->h = h; - surface->cs = cs; + surface->cs = colorSpace; vport.x = vport.y = 0; vport.w = surface->w; @@ -661,6 +661,13 @@ SwRenderer::SwRenderer():mpool(globalMpool) } +uint32_t SwRenderer::colorSpace() +{ + if (surface) return surface->cs; + return tvg::SwCanvas::ARGB8888; +} + + bool SwRenderer::init(uint32_t threads) { if ((initEngineCnt++) > 0) return true; diff --git a/libfenrir/src/main/jni/thorvg/src/lib/sw_engine/tvgSwRenderer.h b/libfenrir/src/main/jni/thorvg/src/lib/sw_engine/tvgSwRenderer.h index c3eadbde9..690b7ff0d 100644 --- a/libfenrir/src/main/jni/thorvg/src/lib/sw_engine/tvgSwRenderer.h +++ b/libfenrir/src/main/jni/thorvg/src/lib/sw_engine/tvgSwRenderer.h @@ -50,7 +50,7 @@ class SwRenderer : public RenderMethod bool clear() override; bool sync() override; - bool target(uint32_t* buffer, uint32_t stride, uint32_t w, uint32_t h, uint32_t cs); + bool target(uint32_t* buffer, uint32_t stride, uint32_t w, uint32_t h, uint32_t colorSpace); bool mempool(bool shared); Compositor* target(const RenderRegion& region) override; @@ -58,6 +58,8 @@ class SwRenderer : public RenderMethod bool endComposite(Compositor* cmp) override; void clearCompositors(); + uint32_t colorSpace() override; + static SwRenderer* gen(); static bool init(uint32_t threads); static int32_t init(); diff --git a/libfenrir/src/main/jni/thorvg/src/lib/tvgLoadModule.h b/libfenrir/src/main/jni/thorvg/src/lib/tvgLoadModule.h index 2c2ffda20..4637c90d8 100644 --- a/libfenrir/src/main/jni/thorvg/src/lib/tvgLoadModule.h +++ b/libfenrir/src/main/jni/thorvg/src/lib/tvgLoadModule.h @@ -37,6 +37,7 @@ class LoadModule float vw = 0; float vh = 0; float w = 0, h = 0; //default image size + uint32_t colorSpace = SwCanvas::ARGB8888; virtual ~LoadModule() {} @@ -49,7 +50,7 @@ class LoadModule virtual bool read() = 0; virtual bool close() = 0; - virtual unique_ptr bitmap() { return nullptr; } + virtual unique_ptr bitmap(uint32_t colorSpace) { return nullptr; } virtual unique_ptr paint() { return nullptr; } }; diff --git a/libfenrir/src/main/jni/thorvg/src/lib/tvgPictureImpl.h b/libfenrir/src/main/jni/thorvg/src/lib/tvgPictureImpl.h index b7e2ba038..6ef231322 100644 --- a/libfenrir/src/main/jni/thorvg/src/lib/tvgPictureImpl.h +++ b/libfenrir/src/main/jni/thorvg/src/lib/tvgPictureImpl.h @@ -69,6 +69,7 @@ struct Picture::Impl void* rdata = nullptr; //engine data float w = 0, h = 0; bool resizing = false; + uint32_t rendererColorSpace = 0; ~Impl() { @@ -104,7 +105,7 @@ struct Picture::Impl } } free(surface); - if ((surface = loader->bitmap().release())) { + if ((surface = loader->bitmap(rendererColorSpace).release())) { loader->close(); return RenderUpdateFlag::Image; } @@ -128,6 +129,7 @@ struct Picture::Impl void* update(RenderMethod &renderer, const RenderTransform* pTransform, uint32_t opacity, Array& clips, RenderUpdateFlag pFlag, bool clipper) { + rendererColorSpace = renderer.colorSpace(); auto flag = reload(); if (surface) { diff --git a/libfenrir/src/main/jni/thorvg/src/lib/tvgRender.h b/libfenrir/src/main/jni/thorvg/src/lib/tvgRender.h index e9ad82109..55bbec1fc 100644 --- a/libfenrir/src/main/jni/thorvg/src/lib/tvgRender.h +++ b/libfenrir/src/main/jni/thorvg/src/lib/tvgRender.h @@ -203,6 +203,8 @@ class RenderMethod virtual Compositor* target(const RenderRegion& region) = 0; virtual bool beginComposite(Compositor* cmp, CompositeMethod method, uint32_t opacity) = 0; virtual bool endComposite(Compositor* cmp) = 0; + + virtual uint32_t colorSpace() = 0; }; } diff --git a/libfenrir/src/main/jni/thorvg/src/loaders/raw/tvgRawLoader.cpp b/libfenrir/src/main/jni/thorvg/src/loaders/raw/tvgRawLoader.cpp index d8c05594f..524cff1c5 100644 --- a/libfenrir/src/main/jni/thorvg/src/loaders/raw/tvgRawLoader.cpp +++ b/libfenrir/src/main/jni/thorvg/src/loaders/raw/tvgRawLoader.cpp @@ -29,6 +29,23 @@ /* Internal Class Implementation */ /************************************************************************/ +static inline uint32_t CHANGE_COLORSPACE(uint32_t c) +{ + return (c & 0xff000000) + ((c & 0x00ff0000)>>16) + (c & 0x0000ff00) + ((c & 0x000000ff)<<16); +} + + +static void _changeColorSpace(uint32_t* data, uint32_t w, uint32_t h) +{ + auto buffer = data; + for (uint32_t y = 0; y < h; ++y, buffer += w) { + auto src = buffer; + for (uint32_t x = 0; x < w; ++x, ++src) { + *src = CHANGE_COLORSPACE(*src); + } + } +} + /************************************************************************/ /* External Class Implementation */ /************************************************************************/ @@ -55,7 +72,7 @@ bool RawLoader::open(const uint32_t* data, uint32_t w, uint32_t h, bool copy) if (!content) return false; memcpy((void*)content, data, sizeof(uint32_t) * w * h); } - else content = data; + else content = const_cast(data); return true; } @@ -73,16 +90,20 @@ bool RawLoader::close() } -unique_ptr RawLoader::bitmap() +unique_ptr RawLoader::bitmap(uint32_t colorSpace) { if (!content) return nullptr; + if (this->colorSpace != colorSpace) { + this->colorSpace = colorSpace; + _changeColorSpace(content, w, h); + } auto surface = static_cast(malloc(sizeof(Surface))); - surface->buffer = (uint32_t*)(content); - surface->stride = (uint32_t)w; - surface->w = (uint32_t)w; - surface->h = (uint32_t)h; - surface->cs = SwCanvas::ARGB8888; + surface->buffer = content; + surface->stride = static_cast(w); + surface->w = static_cast(w); + surface->h = static_cast(h); + surface->cs = colorSpace; return unique_ptr(surface); } diff --git a/libfenrir/src/main/jni/thorvg/src/loaders/raw/tvgRawLoader.h b/libfenrir/src/main/jni/thorvg/src/loaders/raw/tvgRawLoader.h index d3810107e..a5e3d5936 100644 --- a/libfenrir/src/main/jni/thorvg/src/loaders/raw/tvgRawLoader.h +++ b/libfenrir/src/main/jni/thorvg/src/loaders/raw/tvgRawLoader.h @@ -26,7 +26,7 @@ class RawLoader : public LoadModule { public: - const uint32_t* content = nullptr; + uint32_t* content = nullptr; bool copy = false; ~RawLoader(); @@ -36,7 +36,7 @@ class RawLoader : public LoadModule bool read() override; bool close() override; - unique_ptr bitmap() override; + unique_ptr bitmap(uint32_t colorSpace) override; }; diff --git a/material/build.gradle b/material/build.gradle index d4024a4cb..2338ed989 100644 --- a/material/build.gradle +++ b/material/build.gradle @@ -1,5 +1,6 @@ plugins { id("com.android.library") + id("kotlin-android") } //1.9.0-alpha01 @@ -89,17 +90,24 @@ android { encoding = "utf-8" aaptOptions.additionalParameters "--no-version-vectors" } + kotlinOptions { + jvmTarget = "1.8" + } lint { checkOnly 'NewApi' } } dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") + implementation("org.jetbrains.kotlin:kotlin-parcelize-runtime:$kotlin_version") + implementation("org.jetbrains.kotlin:kotlin-android-extensions-runtime:$kotlin_version") + compileOnly("org.jetbrains.kotlin:kotlin-annotations-jvm:$kotlin_version") implementation("androidx.annotation:annotation:$annotationVersion") implementation("androidx.appcompat:appcompat:$appcompatVersion") - implementation("androidx.core:core:$coreVersion") - implementation("androidx.activity:activity:$activityVersion") - implementation("androidx.fragment:fragment:$fragmentVersion") + implementation("androidx.core:core-ktx:$coreVersion") + implementation("androidx.activity:activity-ktx:$activityVersion") + implementation("androidx.fragment:fragment-ktx:$fragmentVersion") implementation("androidx.cardview:cardview:$cardviewVersion") implementation("androidx.dynamicanimation:dynamicanimation:$dynamicanimationVersion") implementation("androidx.constraintlayout:constraintlayout:$constraintlayoutVersion") diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/AdapterHelper.java b/viewpager2/src/main/java/androidx/recyclerview/widget/AdapterHelper.java index c439e0ddd..006043e3d 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/AdapterHelper.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/AdapterHelper.java @@ -18,7 +18,6 @@ import android.util.Log; -import androidx.annotation.NonNull; import androidx.core.util.Pools; import java.util.ArrayList; @@ -52,14 +51,22 @@ final class AdapterHelper implements OpReorderer.Callback { private static final boolean DEBUG = false; private static final String TAG = "AHT"; + + private Pools.Pool mUpdateOpPool = new Pools.SimplePool<>(UpdateOp.POOL_SIZE); + final ArrayList mPendingUpdates = new ArrayList<>(); + final ArrayList mPostponedList = new ArrayList<>(); + final Callback mCallback; + + Runnable mOnItemProcessedCallback; + final boolean mDisableRecycler; + final OpReorderer mOpReorderer; - private final Pools.Pool mUpdateOpPool = new Pools.SimplePool<>(UpdateOp.POOL_SIZE); - Runnable mOnItemProcessedCallback; - private int mExistingUpdateTypes; + + private int mExistingUpdateTypes = 0; AdapterHelper(Callback callback) { this(callback, false); @@ -84,7 +91,7 @@ void reset() { void preProcess() { mOpReorderer.reorderOps(mPendingUpdates); - int count = mPendingUpdates.size(); + final int count = mPendingUpdates.size(); for (int i = 0; i < count; i++) { UpdateOp op = mPendingUpdates.get(i); switch (op.cmd) { @@ -109,7 +116,7 @@ void preProcess() { } void consumePostponedUpdates() { - int count = mPostponedList.size(); + final int count = mPostponedList.size(); for (int i = 0; i < count; i++) { mCallback.onDispatchSecondPass(mPostponedList.get(i)); } @@ -241,7 +248,7 @@ private void dispatchAndUpdateViewHolders(UpdateOp op) { } int tmpCnt = 1; int offsetPositionForPartial = op.positionStart; - int positionMultiplier; + final int positionMultiplier; switch (op.cmd) { case UpdateOp.UPDATE: positionMultiplier = 1; @@ -253,7 +260,7 @@ private void dispatchAndUpdateViewHolders(UpdateOp op) { throw new IllegalArgumentException("op should be remove or update." + op); } for (int p = 1; p < op.itemCount; p++) { - int pos = op.positionStart + (positionMultiplier * p); + final int pos = op.positionStart + (positionMultiplier * p); int updatedPos = updatePositionWithPostponed(pos, op.cmd); if (DEBUG) { Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos); @@ -320,7 +327,7 @@ void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) { } private int updatePositionWithPostponed(int pos, int cmd) { - int count = mPostponedList.size(); + final int count = mPostponedList.size(); for (int i = count - 1; i >= 0; i--) { UpdateOp postponed = mPostponedList.get(i); if (postponed.cmd == UpdateOp.MOVE) { @@ -401,7 +408,7 @@ private int updatePositionWithPostponed(int pos, int cmd) { } private boolean canFindInPreLayout(int position) { - int count = mPostponedList.size(); + final int count = mPostponedList.size(); for (int i = 0; i < count; i++) { UpdateOp op = mPostponedList.get(i); if (op.cmd == UpdateOp.MOVE) { @@ -410,7 +417,7 @@ private boolean canFindInPreLayout(int position) { } } else if (op.cmd == UpdateOp.ADD) { // TODO optimize. - int end = op.positionStart + op.itemCount; + final int end = op.positionStart + op.itemCount; for (int pos = op.positionStart; pos < end; pos++) { if (findPositionOffset(pos, i + 1) == position) { return true; @@ -548,7 +555,7 @@ void consumeUpdatesInOnePass() { // we still consume postponed updates (if there is) in case there was a pre-process call // w/o a matching consumePostponedUpdates. consumePostponedUpdates(); - int count = mPendingUpdates.size(); + final int count = mPendingUpdates.size(); for (int i = 0; i < count; i++) { UpdateOp op = mPendingUpdates.get(i); switch (op.cmd) { @@ -578,7 +585,7 @@ void consumeUpdatesInOnePass() { } public int applyPendingUpdatesToPosition(int position) { - int size = mPendingUpdates.size(); + final int size = mPendingUpdates.size(); for (int i = 0; i < size; i++) { UpdateOp op = mPendingUpdates.get(i); switch (op.cmd) { @@ -589,7 +596,7 @@ public int applyPendingUpdatesToPosition(int position) { break; case UpdateOp.REMOVE: if (op.positionStart <= position) { - int end = op.positionStart + op.itemCount; + final int end = op.positionStart + op.itemCount; if (end > position) { return RecyclerView.NO_POSITION; } @@ -617,58 +624,6 @@ boolean hasUpdates() { return !mPostponedList.isEmpty() && !mPendingUpdates.isEmpty(); } - @Override - public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount, Object payload) { - UpdateOp op = mUpdateOpPool.acquire(); - if (op == null) { - op = new UpdateOp(cmd, positionStart, itemCount, payload); - } else { - op.cmd = cmd; - op.positionStart = positionStart; - op.itemCount = itemCount; - op.payload = payload; - } - return op; - } - - @Override - public void recycleUpdateOp(UpdateOp op) { - if (!mDisableRecycler) { - op.payload = null; - mUpdateOpPool.release(op); - } - } - - void recycleUpdateOpsAndClearList(List ops) { - int count = ops.size(); - for (int i = 0; i < count; i++) { - recycleUpdateOp(ops.get(i)); - } - ops.clear(); - } - - /** - * Contract between AdapterHelper and RecyclerView. - */ - interface Callback { - - RecyclerView.ViewHolder findViewHolder(int position); - - void offsetPositionsForRemovingInvisible(int positionStart, int itemCount); - - void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount); - - void markViewHoldersUpdated(int positionStart, int itemCount, Object payloads); - - void onDispatchFirstPass(UpdateOp updateOp); - - void onDispatchSecondPass(UpdateOp updateOp); - - void offsetPositionsForAdd(int positionStart, int itemCount); - - void offsetPositionsForMove(int from, int to); - } - /** * Queued operation to happen when child views are updated. */ @@ -714,7 +669,6 @@ String cmdToString() { return "??"; } - @NonNull @Override public String toString() { return Integer.toHexString(System.identityHashCode(this)) @@ -749,8 +703,14 @@ public boolean equals(Object o) { return false; } if (payload != null) { - return payload.equals(op.payload); - } else return op.payload == null; + if (!payload.equals(op.payload)) { + return false; + } + } else if (op.payload != null) { + return false; + } + + return true; } @Override @@ -761,4 +721,56 @@ public int hashCode() { return result; } } + + @Override + public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount, Object payload) { + UpdateOp op = mUpdateOpPool.acquire(); + if (op == null) { + op = new UpdateOp(cmd, positionStart, itemCount, payload); + } else { + op.cmd = cmd; + op.positionStart = positionStart; + op.itemCount = itemCount; + op.payload = payload; + } + return op; + } + + @Override + public void recycleUpdateOp(UpdateOp op) { + if (!mDisableRecycler) { + op.payload = null; + mUpdateOpPool.release(op); + } + } + + void recycleUpdateOpsAndClearList(List ops) { + final int count = ops.size(); + for (int i = 0; i < count; i++) { + recycleUpdateOp(ops.get(i)); + } + ops.clear(); + } + + /** + * Contract between AdapterHelper and RecyclerView. + */ + interface Callback { + + RecyclerView.ViewHolder findViewHolder(int position); + + void offsetPositionsForRemovingInvisible(int positionStart, int itemCount); + + void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount); + + void markViewHoldersUpdated(int positionStart, int itemCount, Object payloads); + + void onDispatchFirstPass(UpdateOp updateOp); + + void onDispatchSecondPass(UpdateOp updateOp); + + void offsetPositionsForAdd(int positionStart, int itemCount); + + void offsetPositionsForMove(int from, int to); + } } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/AdapterListUpdateCallback.java b/viewpager2/src/main/java/androidx/recyclerview/widget/AdapterListUpdateCallback.java index c64e1d6f1..ec94f9c44 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/AdapterListUpdateCallback.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/AdapterListUpdateCallback.java @@ -38,33 +38,25 @@ public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) { mAdapter = adapter; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public void onInserted(int position, int count) { mAdapter.notifyItemRangeInserted(position, count); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public void onRemoved(int position, int count) { mAdapter.notifyItemRangeRemoved(position, count); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public void onMoved(int fromPosition, int toPosition) { mAdapter.notifyItemMoved(fromPosition, toPosition); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public void onChanged(int position, int count, Object payload) { diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/AsyncDifferConfig.java b/viewpager2/src/main/java/androidx/recyclerview/widget/AsyncDifferConfig.java index 267e6ff99..ccd9cfae6 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/AsyncDifferConfig.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/AsyncDifferConfig.java @@ -50,9 +50,7 @@ public final class AsyncDifferConfig { mDiffCallback = diffCallback; } - /** - * @hide - */ + /** @hide */ @SuppressWarnings("WeakerAccess") @RestrictTo(RestrictTo.Scope.LIBRARY) @Nullable @@ -78,13 +76,10 @@ public DiffUtil.ItemCallback getDiffCallback() { * @param */ public static final class Builder { - // TODO: remove the below once supportlib has its own appropriate executors - private static final Object sExecutorLock = new Object(); - private static Executor sDiffExecutor; - private final DiffUtil.ItemCallback mDiffCallback; @Nullable private Executor mMainThreadExecutor; private Executor mBackgroundThreadExecutor; + private final DiffUtil.ItemCallback mDiffCallback; public Builder(@NonNull DiffUtil.ItemCallback diffCallback) { mDiffCallback = diffCallback; @@ -98,6 +93,7 @@ public Builder(@NonNull DiffUtil.ItemCallback diffCallback) { * * @param executor The executor which can run tasks in the UI thread. * @return this + * * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY) @@ -144,5 +140,9 @@ public AsyncDifferConfig build() { mBackgroundThreadExecutor, mDiffCallback); } + + // TODO: remove the below once supportlib has its own appropriate executors + private static final Object sExecutorLock = new Object(); + private static Executor sDiffExecutor = null; } } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/AsyncListDiffer.java b/viewpager2/src/main/java/androidx/recyclerview/widget/AsyncListDiffer.java index 9b9310e5b..5209a0cb9 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/AsyncListDiffer.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/AsyncListDiffer.java @@ -108,43 +108,59 @@ * } * * @param Type of the lists this AsyncListDiffer will receive. + * * @see DiffUtil * @see AdapterListUpdateCallback */ public class AsyncListDiffer { - // TODO: use MainThreadExecutor from supportlib once one exists - private static final Executor sMainThreadExecutor = new MainThreadExecutor(); - @SuppressWarnings("WeakerAccess") /* synthetic access */ - final AsyncDifferConfig mConfig; - final Executor mMainThreadExecutor; private final ListUpdateCallback mUpdateCallback; - private final List> mListeners = new CopyOnWriteArrayList<>(); - // Max generation of currently scheduled runnable @SuppressWarnings("WeakerAccess") /* synthetic access */ - int mMaxScheduledGeneration; - @Nullable - private List mList; + final AsyncDifferConfig mConfig; + Executor mMainThreadExecutor; + + private static class MainThreadExecutor implements Executor { + final Handler mHandler = new Handler(Looper.getMainLooper()); + MainThreadExecutor() {} + @Override + public void execute(@NonNull Runnable command) { + mHandler.post(command); + } + } + + // TODO: use MainThreadExecutor from supportlib once one exists + private static final Executor sMainThreadExecutor = new MainThreadExecutor(); + /** - * Non-null, unmodifiable version of mList. - *

- * Collections.emptyList when mList is null, wrapped by Collections.unmodifiableList otherwise + * Listener for when the current List is updated. + * + * @param Type of items in List */ - @NonNull - private List mReadOnlyList = Collections.emptyList(); + public interface ListListener { + /** + * Called after the current List has been updated. + * + * @param previousList The previous list. + * @param currentList The new current list. + */ + void onCurrentListChanged(@NonNull List previousList, @NonNull List currentList); + } + + private final List> mListeners = new CopyOnWriteArrayList<>(); /** * Convenience for * {@code AsyncListDiffer(new AdapterListUpdateCallback(adapter), * new AsyncDifferConfig.Builder().setDiffCallback(diffCallback).build());} * - * @param adapter Adapter to dispatch position updates to. + * @param adapter Adapter to dispatch position updates to. * @param diffCallback ItemCallback that compares items to dispatch appropriate animations when + * * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter) */ public AsyncListDiffer(@NonNull RecyclerView.Adapter adapter, - @NonNull DiffUtil.ItemCallback diffCallback) { + @NonNull DiffUtil.ItemCallback diffCallback) { this(new AdapterListUpdateCallback(adapter), - new AsyncDifferConfig.Builder<>(diffCallback).build()); + new AsyncDifferConfig.Builder<>(diffCallback).build()); } /** @@ -152,13 +168,14 @@ public AsyncListDiffer(@NonNull RecyclerView.Adapter adapter, * updates to. * * @param listUpdateCallback Callback to dispatch updates to. - * @param config Config to define background work Executor, and DiffUtil.ItemCallback for - * computing List diffs. + * @param config Config to define background work Executor, and DiffUtil.ItemCallback for + * computing List diffs. + * * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter) */ @SuppressWarnings("WeakerAccess") public AsyncListDiffer(@NonNull ListUpdateCallback listUpdateCallback, - @NonNull AsyncDifferConfig config) { + @NonNull AsyncDifferConfig config) { mUpdateCallback = listUpdateCallback; mConfig = config; if (config.getMainThreadExecutor() != null) { @@ -168,6 +185,21 @@ public AsyncListDiffer(@NonNull ListUpdateCallback listUpdateCallback, } } + @Nullable + private List mList; + + /** + * Non-null, unmodifiable version of mList. + *

+ * Collections.emptyList when mList is null, wrapped by Collections.unmodifiableList otherwise + */ + @NonNull + private List mReadOnlyList = Collections.emptyList(); + + // Max generation of currently scheduled runnable + @SuppressWarnings("WeakerAccess") /* synthetic access */ + int mMaxScheduledGeneration; + /** * Get the current List - any diffing to present this list has already been computed and * dispatched via the ListUpdateCallback. @@ -195,7 +227,7 @@ public List getCurrentList() { * @param newList The new List. */ @SuppressWarnings("WeakerAccess") - public void submitList(@Nullable List newList) { + public void submitList(@Nullable final List newList) { submitList(newList, null); } @@ -211,15 +243,15 @@ public void submitList(@Nullable List newList) { * may not be executed. If List B is submitted immediately after List A, and is * committed directly, the callback associated with List A will not be run. * - * @param newList The new List. + * @param newList The new List. * @param commitCallback Optional runnable that is executed when the List is committed, if * it is committed. */ @SuppressWarnings("WeakerAccess") - public void submitList(@Nullable List newList, - @Nullable Runnable commitCallback) { + public void submitList(@Nullable final List newList, + @Nullable final Runnable commitCallback) { // incrementing generation means any currently-running diffs are discarded when they finish - int runGeneration = ++mMaxScheduledGeneration; + final int runGeneration = ++mMaxScheduledGeneration; if (newList == mList) { // nothing to do (Note - still had to inc generation, since may have ongoing work) @@ -229,10 +261,11 @@ public void submitList(@Nullable List newList, return; } - List previousList = mReadOnlyList; + final List previousList = mReadOnlyList; // fast simple remove all if (newList == null) { + //noinspection ConstantConditions int countRemoved = mList.size(); mList = null; mReadOnlyList = Collections.emptyList(); @@ -252,9 +285,9 @@ public void submitList(@Nullable List newList, return; } - List oldList = mList; + final List oldList = mList; mConfig.getBackgroundThreadExecutor().execute(() -> { - DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { + final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { @Override public int getOldListSize() { return oldList.size(); @@ -322,7 +355,7 @@ void latchList( @NonNull List newList, @NonNull DiffUtil.DiffResult diffResult, @Nullable Runnable commitCallback) { - List previousList = mReadOnlyList; + final List previousList = mReadOnlyList; mList = newList; // notify last, after list is updated mReadOnlyList = Collections.unmodifiableList(newList); @@ -331,7 +364,7 @@ void latchList( } private void onCurrentListChanged(@NonNull List previousList, - @Nullable Runnable commitCallback) { + @Nullable Runnable commitCallback) { // current list is always mReadOnlyList for (ListListener listener : mListeners) { listener.onCurrentListChanged(previousList, mReadOnlyList); @@ -345,6 +378,7 @@ private void onCurrentListChanged(@NonNull List previousList, * Add a ListListener to receive updates when the current List changes. * * @param listener Listener to receive updates. + * * @see #getCurrentList() * @see #removeListListener(ListListener) */ @@ -362,31 +396,4 @@ public void addListListener(@NonNull ListListener listener) { public void removeListListener(@NonNull ListListener listener) { mListeners.remove(listener); } - - /** - * Listener for when the current List is updated. - * - * @param Type of items in List - */ - public interface ListListener { - /** - * Called after the current List has been updated. - * - * @param previousList The previous list. - * @param currentList The new current list. - */ - void onCurrentListChanged(@NonNull List previousList, @NonNull List currentList); - } - - private static class MainThreadExecutor implements Executor { - final Handler mHandler = new Handler(Looper.getMainLooper()); - - MainThreadExecutor() { - } - - @Override - public void execute(@NonNull Runnable command) { - mHandler.post(command); - } - } } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/AsyncListUtil.java b/viewpager2/src/main/java/androidx/recyclerview/widget/AsyncListUtil.java index b87bb6016..43504db93 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/AsyncListUtil.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/AsyncListUtil.java @@ -44,6 +44,7 @@ *

* This class is designed to work with {@link RecyclerView}, but it does * not depend on it and can be used with other list views. + * */ public class AsyncListUtil { static final String TAG = "AsyncListUtil"; @@ -63,24 +64,31 @@ public class AsyncListUtil { final int[] mTmpRange = new int[2]; final int[] mPrevRange = new int[2]; final int[] mTmpRangeExtended = new int[2]; - final SparseIntArray mMissingPositions = new SparseIntArray(); + boolean mAllowScrollHints; - int mItemCount; + private int mScrollHint = ViewCallback.HINT_SCROLL_NONE; + + int mItemCount = 0; - int mDisplayedGeneration; + int mDisplayedGeneration = 0; int mRequestedGeneration = mDisplayedGeneration; - private int mScrollHint = ViewCallback.HINT_SCROLL_NONE; + + final SparseIntArray mMissingPositions = new SparseIntArray(); + + void log(String s, Object... args) { + Log.d(TAG, "[MAIN] " + String.format(s, args)); + } /** * Creates an AsyncListUtil. * - * @param klass Class of the data item. - * @param tileSize Number of item per chunk loaded at once. + * @param klass Class of the data item. + * @param tileSize Number of item per chunk loaded at once. * @param dataCallback Data access callback. * @param viewCallback Callback for querying visible item range and update notifications. */ public AsyncListUtil(@NonNull Class klass, int tileSize, - @NonNull DataCallback dataCallback, @NonNull ViewCallback viewCallback) { + @NonNull DataCallback dataCallback, @NonNull ViewCallback viewCallback) { mTClass = klass; mTileSize = tileSize; mDataCallback = dataCallback; @@ -89,249 +97,12 @@ public AsyncListUtil(@NonNull Class klass, int tileSize, mTileList = new TileList<>(mTileSize); ThreadUtil threadUtil = new MessageThreadUtil<>(); - // Will be set to true after a first real scroll. - // There will be no scroll event if the size change does not affect the current range. - ThreadUtil.MainThreadCallback mMainThreadCallback = new ThreadUtil.MainThreadCallback() { - @Override - public void updateItemCount(int generation, int itemCount) { - if (DEBUG) { - log("updateItemCount: size=%d, gen #%d", itemCount, generation); - } - if (!isRequestedGeneration(generation)) { - return; - } - mItemCount = itemCount; - mViewCallback.onDataRefresh(); - mDisplayedGeneration = mRequestedGeneration; - recycleAllTiles(); - - mAllowScrollHints = false; // Will be set to true after a first real scroll. - // There will be no scroll event if the size change does not affect the current range. - updateRange(); - } - - @Override - public void addTile(int generation, @NonNull TileList.Tile tile) { - if (!isRequestedGeneration(generation)) { - if (DEBUG) { - log("recycling an older generation tile @%d", tile.mStartPosition); - } - mBackgroundProxy.recycleTile(tile); - return; - } - TileList.Tile duplicate = mTileList.addOrReplace(tile); - if (duplicate != null) { - Log.e(TAG, "duplicate tile @" + duplicate.mStartPosition); - mBackgroundProxy.recycleTile(duplicate); - } - if (DEBUG) { - log("gen #%d, added tile @%d, total tiles: %d", - generation, tile.mStartPosition, mTileList.size()); - } - int endPosition = tile.mStartPosition + tile.mItemCount; - int index = 0; - while (index < mMissingPositions.size()) { - int position = mMissingPositions.keyAt(index); - if (tile.mStartPosition <= position && position < endPosition) { - mMissingPositions.removeAt(index); - mViewCallback.onItemLoaded(position); - } else { - index++; - } - } - } - - @Override - public void removeTile(int generation, int position) { - if (!isRequestedGeneration(generation)) { - return; - } - TileList.Tile tile = mTileList.removeAtPos(position); - if (tile == null) { - Log.e(TAG, "tile not found @" + position); - return; - } - if (DEBUG) { - log("recycling tile @%d, total tiles: %d", tile.mStartPosition, mTileList.size()); - } - mBackgroundProxy.recycleTile(tile); - } - - private void recycleAllTiles() { - if (DEBUG) { - log("recycling all %d tiles", mTileList.size()); - } - for (int i = 0; i < mTileList.size(); i++) { - mBackgroundProxy.recycleTile(mTileList.getAtIndex(i)); - } - mTileList.clear(); - } - - private boolean isRequestedGeneration(int generation) { - return generation == mRequestedGeneration; - } - }; mMainThreadProxy = threadUtil.getMainThreadProxy(mMainThreadCallback); - // All pending tile requests are removed by ThreadUtil at this point. - // Re-request all required tiles in the most optimal order. - // Could not flush on either side, bail out. - ThreadUtil.BackgroundCallback mBackgroundCallback = new ThreadUtil.BackgroundCallback() { - - final SparseBooleanArray mLoadedTiles = new SparseBooleanArray(); - private TileList.Tile mRecycledRoot; - private int mGeneration; - private int mItemCount; - - private int mFirstRequiredTileStart; - private int mLastRequiredTileStart; - - @Override - public void refresh(int generation) { - mGeneration = generation; - mLoadedTiles.clear(); - mItemCount = mDataCallback.refreshData(); - mMainThreadProxy.updateItemCount(mGeneration, mItemCount); - } - - @Override - public void updateRange(int rangeStart, int rangeEnd, int extRangeStart, int extRangeEnd, - int scrollHint) { - if (DEBUG) { - log("updateRange: %d..%d extended to %d..%d, scroll hint: %d", - rangeStart, rangeEnd, extRangeStart, extRangeEnd, scrollHint); - } - - if (rangeStart > rangeEnd) { - return; - } - - int firstVisibleTileStart = getTileStart(rangeStart); - int lastVisibleTileStart = getTileStart(rangeEnd); - - mFirstRequiredTileStart = getTileStart(extRangeStart); - mLastRequiredTileStart = getTileStart(extRangeEnd); - if (DEBUG) { - log("requesting tile range: %d..%d", - mFirstRequiredTileStart, mLastRequiredTileStart); - } - - // All pending tile requests are removed by ThreadUtil at this point. - // Re-request all required tiles in the most optimal order. - if (scrollHint == ViewCallback.HINT_SCROLL_DESC) { - requestTiles(mFirstRequiredTileStart, lastVisibleTileStart, scrollHint, true); - requestTiles(lastVisibleTileStart + mTileSize, mLastRequiredTileStart, scrollHint, - false); - } else { - requestTiles(firstVisibleTileStart, mLastRequiredTileStart, scrollHint, false); - requestTiles(mFirstRequiredTileStart, firstVisibleTileStart - mTileSize, scrollHint, - true); - } - } - - private int getTileStart(int position) { - return position - position % mTileSize; - } - - private void requestTiles(int firstTileStart, int lastTileStart, int scrollHint, - boolean backwards) { - for (int i = firstTileStart; i <= lastTileStart; i += mTileSize) { - int tileStart = backwards ? (lastTileStart + firstTileStart - i) : i; - if (DEBUG) { - log("requesting tile @%d", tileStart); - } - mBackgroundProxy.loadTile(tileStart, scrollHint); - } - } - - @Override - public void loadTile(int position, int scrollHint) { - if (isTileLoaded(position)) { - if (DEBUG) { - log("already loaded tile @%d", position); - } - return; - } - TileList.Tile tile = acquireTile(); - tile.mStartPosition = position; - tile.mItemCount = Math.min(mTileSize, mItemCount - tile.mStartPosition); - mDataCallback.fillData(tile.mItems, tile.mStartPosition, tile.mItemCount); - flushTileCache(scrollHint); - addTile(tile); - } - - @Override - public void recycleTile(@NonNull TileList.Tile tile) { - if (DEBUG) { - log("recycling tile @%d", tile.mStartPosition); - } - mDataCallback.recycleData(tile.mItems, tile.mItemCount); - - tile.mNext = mRecycledRoot; - mRecycledRoot = tile; - } - - private TileList.Tile acquireTile() { - if (mRecycledRoot != null) { - TileList.Tile result = mRecycledRoot; - mRecycledRoot = mRecycledRoot.mNext; - return result; - } - return new TileList.Tile<>(mTClass, mTileSize); - } - - private boolean isTileLoaded(int position) { - return mLoadedTiles.get(position); - } - - private void addTile(TileList.Tile tile) { - mLoadedTiles.put(tile.mStartPosition, true); - mMainThreadProxy.addTile(mGeneration, tile); - if (DEBUG) { - log("loaded tile @%d, total tiles: %d", tile.mStartPosition, mLoadedTiles.size()); - } - } - - private void removeTile(int position) { - mLoadedTiles.delete(position); - mMainThreadProxy.removeTile(mGeneration, position); - if (DEBUG) { - log("flushed tile @%d, total tiles: %s", position, mLoadedTiles.size()); - } - } - - private void flushTileCache(int scrollHint) { - int cacheSizeLimit = mDataCallback.getMaxCachedTiles(); - while (mLoadedTiles.size() >= cacheSizeLimit) { - int firstLoadedTileStart = mLoadedTiles.keyAt(0); - int lastLoadedTileStart = mLoadedTiles.keyAt(mLoadedTiles.size() - 1); - int startMargin = mFirstRequiredTileStart - firstLoadedTileStart; - int endMargin = lastLoadedTileStart - mLastRequiredTileStart; - if (startMargin > 0 && (startMargin >= endMargin || - (scrollHint == ViewCallback.HINT_SCROLL_ASC))) { - removeTile(firstLoadedTileStart); - } else if (endMargin > 0 && (startMargin < endMargin || - (scrollHint == ViewCallback.HINT_SCROLL_DESC))) { - removeTile(lastLoadedTileStart); - } else { - // Could not flush on either side, bail out. - return; - } - } - } - - private void log(String s, Object... args) { - Log.d(TAG, "[BKGR] " + String.format(s, args)); - } - }; mBackgroundProxy = threadUtil.getBackgroundProxy(mBackgroundCallback); refresh(); } - void log(String s, Object... args) { - Log.d(TAG, "[MAIN] " + String.format(s, args)); - } - private boolean isRefreshPending() { return mRequestedGeneration != mDisplayedGeneration; } @@ -375,8 +146,9 @@ public void refresh() { * this position. * * @param position Item position. + * * @return The data item at the given position or null if it has not been loaded - * yet. + * yet. */ @Nullable public T getItem(int position) { @@ -436,6 +208,240 @@ void updateRange() { mTmpRangeExtended[0], mTmpRangeExtended[1], mScrollHint); } + private final ThreadUtil.MainThreadCallback + mMainThreadCallback = new ThreadUtil.MainThreadCallback() { + @Override + public void updateItemCount(int generation, int itemCount) { + if (DEBUG) { + log("updateItemCount: size=%d, gen #%d", itemCount, generation); + } + if (!isRequestedGeneration(generation)) { + return; + } + mItemCount = itemCount; + mViewCallback.onDataRefresh(); + mDisplayedGeneration = mRequestedGeneration; + recycleAllTiles(); + + mAllowScrollHints = false; // Will be set to true after a first real scroll. + // There will be no scroll event if the size change does not affect the current range. + updateRange(); + } + + @Override + public void addTile(int generation, TileList.Tile tile) { + if (!isRequestedGeneration(generation)) { + if (DEBUG) { + log("recycling an older generation tile @%d", tile.mStartPosition); + } + mBackgroundProxy.recycleTile(tile); + return; + } + TileList.Tile duplicate = mTileList.addOrReplace(tile); + if (duplicate != null) { + Log.e(TAG, "duplicate tile @" + duplicate.mStartPosition); + mBackgroundProxy.recycleTile(duplicate); + } + if (DEBUG) { + log("gen #%d, added tile @%d, total tiles: %d", + generation, tile.mStartPosition, mTileList.size()); + } + int endPosition = tile.mStartPosition + tile.mItemCount; + int index = 0; + while (index < mMissingPositions.size()) { + final int position = mMissingPositions.keyAt(index); + if (tile.mStartPosition <= position && position < endPosition) { + mMissingPositions.removeAt(index); + mViewCallback.onItemLoaded(position); + } else { + index++; + } + } + } + + @Override + public void removeTile(int generation, int position) { + if (!isRequestedGeneration(generation)) { + return; + } + TileList.Tile tile = mTileList.removeAtPos(position); + if (tile == null) { + Log.e(TAG, "tile not found @" + position); + return; + } + if (DEBUG) { + log("recycling tile @%d, total tiles: %d", tile.mStartPosition, mTileList.size()); + } + mBackgroundProxy.recycleTile(tile); + } + + private void recycleAllTiles() { + if (DEBUG) { + log("recycling all %d tiles", mTileList.size()); + } + for (int i = 0; i < mTileList.size(); i++) { + mBackgroundProxy.recycleTile(mTileList.getAtIndex(i)); + } + mTileList.clear(); + } + + private boolean isRequestedGeneration(int generation) { + return generation == mRequestedGeneration; + } + }; + + private final ThreadUtil.BackgroundCallback + mBackgroundCallback = new ThreadUtil.BackgroundCallback() { + + private TileList.Tile mRecycledRoot; + + final SparseBooleanArray mLoadedTiles = new SparseBooleanArray(); + + private int mGeneration; + private int mItemCount; + + private int mFirstRequiredTileStart; + private int mLastRequiredTileStart; + + @Override + public void refresh(int generation) { + mGeneration = generation; + mLoadedTiles.clear(); + mItemCount = mDataCallback.refreshData(); + mMainThreadProxy.updateItemCount(mGeneration, mItemCount); + } + + @Override + public void updateRange(int rangeStart, int rangeEnd, int extRangeStart, int extRangeEnd, + int scrollHint) { + if (DEBUG) { + log("updateRange: %d..%d extended to %d..%d, scroll hint: %d", + rangeStart, rangeEnd, extRangeStart, extRangeEnd, scrollHint); + } + + if (rangeStart > rangeEnd) { + return; + } + + final int firstVisibleTileStart = getTileStart(rangeStart); + final int lastVisibleTileStart = getTileStart(rangeEnd); + + mFirstRequiredTileStart = getTileStart(extRangeStart); + mLastRequiredTileStart = getTileStart(extRangeEnd); + if (DEBUG) { + log("requesting tile range: %d..%d", + mFirstRequiredTileStart, mLastRequiredTileStart); + } + + // All pending tile requests are removed by ThreadUtil at this point. + // Re-request all required tiles in the most optimal order. + if (scrollHint == ViewCallback.HINT_SCROLL_DESC) { + requestTiles(mFirstRequiredTileStart, lastVisibleTileStart, scrollHint, true); + requestTiles(lastVisibleTileStart + mTileSize, mLastRequiredTileStart, scrollHint, + false); + } else { + requestTiles(firstVisibleTileStart, mLastRequiredTileStart, scrollHint, false); + requestTiles(mFirstRequiredTileStart, firstVisibleTileStart - mTileSize, scrollHint, + true); + } + } + + private int getTileStart(int position) { + return position - position % mTileSize; + } + + private void requestTiles(int firstTileStart, int lastTileStart, int scrollHint, + boolean backwards) { + for (int i = firstTileStart; i <= lastTileStart; i += mTileSize) { + int tileStart = backwards ? (lastTileStart + firstTileStart - i) : i; + if (DEBUG) { + log("requesting tile @%d", tileStart); + } + mBackgroundProxy.loadTile(tileStart, scrollHint); + } + } + + @Override + public void loadTile(int position, int scrollHint) { + if (isTileLoaded(position)) { + if (DEBUG) { + log("already loaded tile @%d", position); + } + return; + } + TileList.Tile tile = acquireTile(); + tile.mStartPosition = position; + tile.mItemCount = Math.min(mTileSize, mItemCount - tile.mStartPosition); + mDataCallback.fillData(tile.mItems, tile.mStartPosition, tile.mItemCount); + flushTileCache(scrollHint); + addTile(tile); + } + + @Override + public void recycleTile(TileList.Tile tile) { + if (DEBUG) { + log("recycling tile @%d", tile.mStartPosition); + } + mDataCallback.recycleData(tile.mItems, tile.mItemCount); + + tile.mNext = mRecycledRoot; + mRecycledRoot = tile; + } + + private TileList.Tile acquireTile() { + if (mRecycledRoot != null) { + TileList.Tile result = mRecycledRoot; + mRecycledRoot = mRecycledRoot.mNext; + return result; + } + return new TileList.Tile<>(mTClass, mTileSize); + } + + private boolean isTileLoaded(int position) { + return mLoadedTiles.get(position); + } + + private void addTile(TileList.Tile tile) { + mLoadedTiles.put(tile.mStartPosition, true); + mMainThreadProxy.addTile(mGeneration, tile); + if (DEBUG) { + log("loaded tile @%d, total tiles: %d", tile.mStartPosition, mLoadedTiles.size()); + } + } + + private void removeTile(int position) { + mLoadedTiles.delete(position); + mMainThreadProxy.removeTile(mGeneration, position); + if (DEBUG) { + log("flushed tile @%d, total tiles: %s", position, mLoadedTiles.size()); + } + } + + private void flushTileCache(int scrollHint) { + final int cacheSizeLimit = mDataCallback.getMaxCachedTiles(); + while (mLoadedTiles.size() >= cacheSizeLimit) { + int firstLoadedTileStart = mLoadedTiles.keyAt(0); + int lastLoadedTileStart = mLoadedTiles.keyAt(mLoadedTiles.size() - 1); + int startMargin = mFirstRequiredTileStart - firstLoadedTileStart; + int endMargin = lastLoadedTileStart - mLastRequiredTileStart; + if (startMargin > 0 && (startMargin >= endMargin || + (scrollHint == ViewCallback.HINT_SCROLL_ASC))) { + removeTile(firstLoadedTileStart); + } else if (endMargin > 0 && (startMargin < endMargin || + (scrollHint == ViewCallback.HINT_SCROLL_DESC))){ + removeTile(lastLoadedTileStart); + } else { + // Could not flush on either side, bail out. + return; + } + } + } + + private void log(String s, Object... args) { + Log.d(TAG, "[BKGR] " + String.format(s, args)); + } + }; + /** * The callback that provides data access for {@link AsyncListUtil}. * @@ -464,9 +470,9 @@ public static abstract class DataCallback { * It is suggested to re-use these objects if possible in your use case. * * @param startPosition The start position in the list. - * @param itemCount The data item count. - * @param data The data item array to fill into. Should not be accessed beyond - * itemCount. + * @param itemCount The data item count. + * @param data The data item array to fill into. Should not be accessed beyond + * itemCount. */ @WorkerThread public abstract void fillData(@NonNull T[] data, int startPosition, int itemCount); @@ -474,7 +480,8 @@ public static abstract class DataCallback { /** * Recycle the objects created in {@link #fillData} if necessary. * - * @param data Array of data items. Should not be accessed beyond itemCount. + * + * @param data Array of data items. Should not be accessed beyond itemCount. * @param itemCount The data item count. */ @WorkerThread @@ -510,7 +517,7 @@ public int getMaxCachedTiles() { * *

* All methods are called on the main thread. - */ + */ public static abstract class ViewCallback { /** @@ -561,14 +568,14 @@ public static abstract class ViewCallback { * However, if scrollHint is {@link #HINT_SCROLL_NONE}, then * outRange will be {50, 250} * - * @param range Visible item range. - * @param outRange Extended range. + * @param range Visible item range. + * @param outRange Extended range. * @param scrollHint The scroll direction hint. */ @UiThread public void extendRangeInto(@NonNull int[] range, @NonNull int[] outRange, int scrollHint) { - int fullRange = range[1] - range[0] + 1; - int halfRange = fullRange / 2; + final int fullRange = range[1] - range[0] + 1; + final int halfRange = fullRange / 2; outRange[0] = range[0] - (scrollHint == HINT_SCROLL_DESC ? fullRange : halfRange); outRange[1] = range[1] + (scrollHint == HINT_SCROLL_ASC ? fullRange : halfRange); } @@ -581,7 +588,6 @@ public void extendRangeInto(@NonNull int[] range, @NonNull int[] outRange, int s /** * Called when an item at the given position is loaded. - * * @param position Item position. */ @UiThread diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/BatchingListUpdateCallback.java b/viewpager2/src/main/java/androidx/recyclerview/widget/BatchingListUpdateCallback.java index a19c54f1d..bad8cc942 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/BatchingListUpdateCallback.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/BatchingListUpdateCallback.java @@ -43,7 +43,7 @@ public class BatchingListUpdateCallback implements ListUpdateCallback { int mLastEventType = TYPE_NONE; int mLastEventPosition = -1; int mLastEventCount = -1; - Object mLastEventPayload; + Object mLastEventPayload = null; public BatchingListUpdateCallback(@NonNull ListUpdateCallback callback) { mWrapped = callback; diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/ChildHelper.java b/viewpager2/src/main/java/androidx/recyclerview/widget/ChildHelper.java index 0a098d867..198b70f95 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/ChildHelper.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/ChildHelper.java @@ -20,8 +20,6 @@ import android.view.View; import android.view.ViewGroup; -import androidx.annotation.NonNull; - import java.util.ArrayList; import java.util.List; @@ -56,7 +54,7 @@ class ChildHelper { /** * Marks a child view as hidden * - * @param child View to hide. + * @param child View to hide. */ private void hideViewInternal(View child) { mHiddenViews.add(child); @@ -66,7 +64,7 @@ private void hideViewInternal(View child) { /** * Unmarks a child view as hidden. * - * @param child View to hide. + * @param child View to hide. */ private boolean unhideViewInternal(View child) { if (mHiddenViews.remove(child)) { @@ -96,7 +94,7 @@ void addView(View child, boolean hidden) { * @param hidden If set to true, this item will be invisible from regular methods. */ void addView(View child, int index, boolean hidden) { - int offset; + final int offset; if (index < 0) { offset = mCallback.getChildCount(); } else { @@ -116,11 +114,11 @@ private int getOffset(int index) { if (index < 0) { return -1; //anything below 0 won't work as diff will be undefined. } - int limit = mCallback.getChildCount(); + final int limit = mCallback.getChildCount(); int offset = index; while (offset < limit) { - int removedBefore = mBucket.countOnesBefore(offset); - int diff = index - (offset - removedBefore); + final int removedBefore = mBucket.countOnesBefore(offset); + final int diff = index - (offset - removedBefore); if (diff == 0) { while (mBucket.get(offset)) { // ensure this offset is not hidden offset++; @@ -159,8 +157,8 @@ void removeView(View view) { * ChildHelper offsets this index to actual ViewGroup index. */ void removeViewAt(int index) { - int offset = getOffset(index); - View view = mCallback.getChildAt(offset); + final int offset = getOffset(index); + final View view = mCallback.getChildAt(offset); if (view == null) { return; } @@ -179,7 +177,7 @@ void removeViewAt(int index) { * @param index Index of the child to return in regular perspective. */ View getChildAt(int index) { - int offset = getOffset(index); + final int offset = getOffset(index); return mCallback.getChildAt(offset); } @@ -202,12 +200,12 @@ void removeAllViewsUnfiltered() { * This can be used to find a disappearing view by position. * * @param position The adapter position of the item. - * @return A hidden view with a valid ViewHolder that matches the position. + * @return A hidden view with a valid ViewHolder that matches the position. */ View findHiddenNonRemovedView(int position) { - int count = mHiddenViews.size(); + final int count = mHiddenViews.size(); for (int i = 0; i < count; i++) { - View view = mHiddenViews.get(i); + final View view = mHiddenViews.get(i); RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view); if (holder.getLayoutPosition() == position && !holder.isInvalid() @@ -227,8 +225,8 @@ View findHiddenNonRemovedView(int position) { * @param hidden If set to true, this item will be invisible to the regular methods. */ void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams, - boolean hidden) { - int offset; + boolean hidden) { + final int offset; if (index < 0) { offset = mCallback.getChildCount(); } else { @@ -281,7 +279,7 @@ View getUnfilteredChildAt(int index) { * @param index Index of the child to return in regular perspective. */ void detachViewFromParent(int index) { - int offset = getOffset(index); + final int offset = getOffset(index); mBucket.remove(offset); mCallback.detachViewFromParent(offset); if (DEBUG) { @@ -296,7 +294,7 @@ void detachViewFromParent(int index) { * @return The regular perspective index of the child or -1 if it does not exists. */ int indexOfChild(View child) { - int index = mCallback.indexOfChild(child); + final int index = mCallback.indexOfChild(child); if (index == -1) { return -1; } @@ -327,7 +325,7 @@ boolean isHidden(View view) { * @param view The view to hide. */ void hide(View view) { - int offset = mCallback.indexOfChild(view); + final int offset = mCallback.indexOfChild(view); if (offset < 0) { throw new IllegalArgumentException("view is not a child, cannot hide " + view); } @@ -349,7 +347,7 @@ void hide(View view) { * @param view The hidden View to unhide */ void unhide(View view) { - int offset = mCallback.indexOfChild(view); + final int offset = mCallback.indexOfChild(view); if (offset < 0) { throw new IllegalArgumentException("view is not a child, cannot hide " + view); } @@ -360,10 +358,9 @@ void unhide(View view) { unhideViewInternal(view); } - @NonNull @Override public String toString() { - return mBucket + ", hidden list:" + mHiddenViews.size(); + return mBucket.toString() + ", hidden list:" + mHiddenViews.size(); } /** @@ -373,7 +370,7 @@ public String toString() { * @return True if the View is found and it is hidden. False otherwise. */ boolean removeViewIfHidden(View view) { - int index = mCallback.indexOfChild(view); + final int index = mCallback.indexOfChild(view); if (index == -1) { if (unhideViewInternal(view) && DEBUG) { throw new IllegalStateException("view is in hidden list but not in view group"); @@ -392,31 +389,6 @@ boolean removeViewIfHidden(View view) { return false; } - interface Callback { - - int getChildCount(); - - void addView(View child, int index); - - int indexOfChild(View view); - - void removeViewAt(int index); - - View getChildAt(int offset); - - void removeAllViews(); - - RecyclerView.ViewHolder getChildViewHolder(View view); - - void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams); - - void detachViewFromParent(int offset); - - void onEnteredHiddenState(View child); - - void onLeftHiddenState(View child); - } - /** * Bitset implementation that provides methods to offset indices. */ @@ -426,7 +398,7 @@ static class Bucket { static final long LAST_BIT = 1L << (Long.SIZE - 1); - long mData; + long mData = 0; Bucket mNext; @@ -477,10 +449,10 @@ void insert(int index, boolean value) { ensureNext(); mNext.insert(index - BITS_PER_WORD, value); } else { - boolean lastBit = (mData & LAST_BIT) != 0; + final boolean lastBit = (mData & LAST_BIT) != 0; long mask = (1L << index) - 1; - long before = mData & mask; - long after = (mData & ~mask) << 1; + final long before = mData & mask; + final long after = (mData & ~mask) << 1; mData = before | after; if (value) { set(index); @@ -500,12 +472,12 @@ boolean remove(int index) { return mNext.remove(index - BITS_PER_WORD); } else { long mask = (1L << index); - boolean value = (mData & mask) != 0; + final boolean value = (mData & mask) != 0; mData &= ~mask; mask = mask - 1; - long before = mData & mask; + final long before = mData & mask; // cannot use >> because it adds one. - long after = Long.rotateRight(mData & ~mask, 1); + final long after = Long.rotateRight(mData & ~mask, 1); mData = before | after; if (mNext != null) { if (mNext.get(0)) { @@ -531,11 +503,35 @@ int countOnesBefore(int index) { } } - @NonNull @Override public String toString() { return mNext == null ? Long.toBinaryString(mData) - : mNext + "xx" + Long.toBinaryString(mData); + : mNext.toString() + "xx" + Long.toBinaryString(mData); } } + + interface Callback { + + int getChildCount(); + + void addView(View child, int index); + + int indexOfChild(View view); + + void removeViewAt(int index); + + View getChildAt(int offset); + + void removeAllViews(); + + RecyclerView.ViewHolder getChildViewHolder(View view); + + void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams); + + void detachViewFromParent(int offset); + + void onEnteredHiddenState(View child); + + void onLeftHiddenState(View child); + } } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/ConcatAdapter.java b/viewpager2/src/main/java/androidx/recyclerview/widget/ConcatAdapter.java index ccf6e8725..45b719fe7 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/ConcatAdapter.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/ConcatAdapter.java @@ -206,7 +206,7 @@ public void setHasStableIds(boolean hasStableIds) { /** * Calling this method is an error and will result in an {@link UnsupportedOperationException}. - *

+ * * ConcatAdapter infers this value from added {@link Adapter}s. * * @param strategy The saved state restoration strategy for this Adapter such that @@ -283,7 +283,7 @@ public List> getAdapters() { /** * Returns the position of the given {@link ViewHolder} in the given {@link Adapter}. - *

+ * * If the given {@link Adapter} is not part of this {@link ConcatAdapter}, * {@link RecyclerView#NO_POSITION} is returned. * @@ -306,7 +306,7 @@ public int findRelativeAdapterPositionIn( /** * Retrieve the adapter and local position for a given position in this {@code ConcatAdapter}. - *

+ * * This allows for retrieving wrapped adapter information in situations where you don't have a * {@link ViewHolder}, such as within a * {@link androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup} in which you want to @@ -316,12 +316,12 @@ public int findRelativeAdapterPositionIn( * @return a Pair with the first element set to the wrapped {@code Adapter} containing that * position and the second element set to the local position in the wrapped adapter * @throws IllegalArgumentException if the specified {@code globalPosition} does not - * correspond to a valid element of this adapter. That is, if {@code globalPosition} is less - * than 0 or greater than the total number of items in the {@code ConcatAdapter} + * correspond to a valid element of this adapter. That is, if {@code globalPosition} is less + * than 0 or greater than the total number of items in the {@code ConcatAdapter} */ @NonNull public Pair, Integer> getWrappedAdapterAndPosition(int - globalPosition) { + globalPosition) { return mController.getWrappedAdapterAndPosition(globalPosition); } @@ -329,13 +329,6 @@ public Pair, Integer> getWrappedAdapterAndPosition * The configuration object for a {@link ConcatAdapter}. */ public static final class Config { - /** - * Default configuration for {@link ConcatAdapter} where {@link Config#isolateViewTypes} - * is set to {@code true} and {@link Config#stableIdMode} is set to - * {@link StableIdMode#NO_STABLE_IDS}. - */ - @NonNull - public static final Config DEFAULT = new Config(true, NO_STABLE_IDS); /** * If {@code false}, {@link ConcatAdapter} assumes all assigned adapters share a global * view type pool such that they use the same view types to refer to the same @@ -345,21 +338,22 @@ public static final class Config { * it also means these adapters should not have conflicting view types * ({@link Adapter#getItemViewType(int)}) such that two different adapters return the same * view type for different {@link ViewHolder}s. - *

+ * * By default, it is set to {@code true} which means {@link ConcatAdapter} will isolate * view types across adapters, preventing them from using the same {@link ViewHolder}s. */ public final boolean isolateViewTypes; + /** * Defines whether the {@link ConcatAdapter} should support stable ids or not * ({@link Adapter#hasStableIds()}. *

* There are 3 possible options: - *

+ * * {@link StableIdMode#NO_STABLE_IDS}: In this mode, {@link ConcatAdapter} ignores the * stable * ids reported by sub adapters. This is the default mode. - *

+ * * {@link StableIdMode#ISOLATED_STABLE_IDS}: In this mode, {@link ConcatAdapter} will return * {@code true} from {@link ConcatAdapter#hasStableIds()} and will require all added * {@link Adapter}s to have stable ids. As two different adapters may return same stable ids @@ -368,19 +362,28 @@ public static final class Config { * id before reporting back to the {@link RecyclerView}. In this mode, the value returned * from {@link ViewHolder#getItemId()} might differ from the value returned from * {@link Adapter#getItemId(int)}. - *

+ * * {@link StableIdMode#SHARED_STABLE_IDS}: In this mode, {@link ConcatAdapter} will return * {@code true} from {@link ConcatAdapter#hasStableIds()} and will require all added * {@link Adapter}s to have stable ids. Unlike {@link StableIdMode#ISOLATED_STABLE_IDS}, * {@link ConcatAdapter} will not override the returned item ids. In this mode, * child {@link Adapter}s must be aware of each-other and never return the same id unless * an item is moved between {@link Adapter}s. - *

+ * * Default value is {@link StableIdMode#NO_STABLE_IDS}. */ @NonNull public final StableIdMode stableIdMode; + + /** + * Default configuration for {@link ConcatAdapter} where {@link Config#isolateViewTypes} + * is set to {@code true} and {@link Config#stableIdMode} is set to + * {@link StableIdMode#NO_STABLE_IDS}. + */ + @NonNull + public static final Config DEFAULT = new Config(true, NO_STABLE_IDS); + Config(boolean isolateViewTypes, @NonNull StableIdMode stableIdMode) { this.isolateViewTypes = isolateViewTypes; this.stableIdMode = stableIdMode; @@ -406,7 +409,7 @@ public enum StableIdMode { * the reported stable id before reporting back to the {@link RecyclerView}. In this * mode, the value returned from {@link ViewHolder#getItemId()} might differ from the * value returned from {@link Adapter#getItemId(int)}. - *

+ * * Adding an adapter without stable ids will result in an * {@link IllegalArgumentException}. */ diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/ConcatAdapterController.java b/viewpager2/src/main/java/androidx/recyclerview/widget/ConcatAdapterController.java index ff3572f5e..d31540c64 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/ConcatAdapterController.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/ConcatAdapterController.java @@ -58,7 +58,7 @@ class ConcatAdapterController implements NestedAdapterWrapper.Callback { * any adapter that was added later on. * Probably does not need to be a weak reference but playing safe here. */ - private final List> mAttachedRecyclerViews = new ArrayList<>(); + private List> mAttachedRecyclerViews = new ArrayList<>(); /** * Keeps the information about which ViewHolder is bound by which adapter. @@ -67,15 +67,18 @@ class ConcatAdapterController implements NestedAdapterWrapper.Callback { private final IdentityHashMap mBinderLookup = new IdentityHashMap<>(); - private final List mWrappers = new ArrayList<>(); + private List mWrappers = new ArrayList<>(); + + // keep one of these around so that we can return wrapper & position w/o allocation ¯\_(ツ)_/¯ + private WrapperAndLocalPosition mReusableHolder = new WrapperAndLocalPosition(); + @NonNull private final ConcatAdapter.Config.StableIdMode mStableIdMode; + /** * This is where we keep stable ids, if supported */ private final StableIdStorage mStableIdStorage; - // keep one of these around so that we can return wrapper & position w/o allocation ¯\_(ツ)_/¯ - private WrapperAndLocalPosition mReusableHolder = new WrapperAndLocalPosition(); ConcatAdapterController( ConcatAdapter concatAdapter, @@ -104,7 +107,7 @@ class ConcatAdapterController implements NestedAdapterWrapper.Callback { @Nullable private NestedAdapterWrapper findWrapperFor(Adapter adapter) { - int index = indexOfWrapper(adapter); + final int index = indexOfWrapper(adapter); if (index == -1) { return null; } @@ -112,7 +115,7 @@ private NestedAdapterWrapper findWrapperFor(Adapter adapter) { } private int indexOfWrapper(Adapter adapter) { - int limit = mWrappers.size(); + final int limit = mWrappers.size(); for (int i = 0; i < limit; i++) { if (mWrappers.get(i).adapter == adapter) { return i; @@ -144,7 +147,7 @@ boolean addAdapter(int index, Adapter adapter) { if (hasStableIds()) { Preconditions.checkArgument(adapter.hasStableIds(), "All sub adapters must have stable ids when stable id mode " - + "is ISOLATED_STABLE_IDS or SHARED_STABLE_IDS"); + + "is ISOLATED_STABLE_IDS or SHARED_STABLE_IDS"); } else { if (adapter.hasStableIds()) { Log.w(ConcatAdapter.TAG, "Stable ids in the adapter will be ignored as the" @@ -178,7 +181,7 @@ boolean addAdapter(int index, Adapter adapter) { } boolean removeAdapter(Adapter adapter) { - int index = indexOfWrapper(adapter); + final int index = indexOfWrapper(adapter); if (index == -1) { return false; } @@ -226,8 +229,8 @@ public void onChanged(@NonNull NestedAdapterWrapper wrapper) { @Override public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrapper, - int positionStart, int itemCount) { - int offset = countItemsBefore(nestedAdapterWrapper); + int positionStart, int itemCount) { + final int offset = countItemsBefore(nestedAdapterWrapper); mConcatAdapter.notifyItemRangeChanged( positionStart + offset, itemCount @@ -236,8 +239,8 @@ public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrappe @Override public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrapper, - int positionStart, int itemCount, @Nullable Object payload) { - int offset = countItemsBefore(nestedAdapterWrapper); + int positionStart, int itemCount, @Nullable Object payload) { + final int offset = countItemsBefore(nestedAdapterWrapper); mConcatAdapter.notifyItemRangeChanged( positionStart + offset, itemCount, @@ -247,8 +250,8 @@ public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrappe @Override public void onItemRangeInserted(@NonNull NestedAdapterWrapper nestedAdapterWrapper, - int positionStart, int itemCount) { - int offset = countItemsBefore(nestedAdapterWrapper); + int positionStart, int itemCount) { + final int offset = countItemsBefore(nestedAdapterWrapper); mConcatAdapter.notifyItemRangeInserted( positionStart + offset, itemCount @@ -257,7 +260,7 @@ public void onItemRangeInserted(@NonNull NestedAdapterWrapper nestedAdapterWrapp @Override public void onItemRangeRemoved(@NonNull NestedAdapterWrapper nestedAdapterWrapper, - int positionStart, int itemCount) { + int positionStart, int itemCount) { int offset = countItemsBefore(nestedAdapterWrapper); mConcatAdapter.notifyItemRangeRemoved( positionStart + offset, @@ -267,7 +270,7 @@ public void onItemRangeRemoved(@NonNull NestedAdapterWrapper nestedAdapterWrappe @Override public void onItemRangeMoved(@NonNull NestedAdapterWrapper nestedAdapterWrapper, - int fromPosition, int toPosition) { + int fromPosition, int toPosition) { int offset = countItemsBefore(nestedAdapterWrapper); mConcatAdapter.notifyItemMoved( fromPosition + offset, @@ -411,7 +414,7 @@ public boolean onFailedToRecycleView(ViewHolder holder) { throw new IllegalStateException("Cannot find wrapper for " + holder + ", seems like it is not bound by this adapter: " + this); } - boolean result = wrapper.adapter.onFailedToRecycleView(holder); + final boolean result = wrapper.adapter.onFailedToRecycleView(holder); mBinderLookup.remove(holder); return result; } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/DefaultItemAnimator.java b/viewpager2/src/main/java/androidx/recyclerview/widget/DefaultItemAnimator.java index fd80a316e..1adf855ad 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/DefaultItemAnimator.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/DefaultItemAnimator.java @@ -40,17 +40,64 @@ public class DefaultItemAnimator extends SimpleItemAnimator { private static final boolean DEBUG = false; private static TimeInterpolator sDefaultInterpolator; - final ArrayList> mAdditionsList = new ArrayList<>(); - final ArrayList> mMovesList = new ArrayList<>(); - final ArrayList> mChangesList = new ArrayList<>(); - final ArrayList mAddAnimations = new ArrayList<>(); - final ArrayList mMoveAnimations = new ArrayList<>(); - final ArrayList mRemoveAnimations = new ArrayList<>(); - final ArrayList mChangeAnimations = new ArrayList<>(); - private final ArrayList mPendingRemovals = new ArrayList<>(); - private final ArrayList mPendingAdditions = new ArrayList<>(); - private final ArrayList mPendingMoves = new ArrayList<>(); - private final ArrayList mPendingChanges = new ArrayList<>(); + + private ArrayList mPendingRemovals = new ArrayList<>(); + private ArrayList mPendingAdditions = new ArrayList<>(); + private ArrayList mPendingMoves = new ArrayList<>(); + private ArrayList mPendingChanges = new ArrayList<>(); + + ArrayList> mAdditionsList = new ArrayList<>(); + ArrayList> mMovesList = new ArrayList<>(); + ArrayList> mChangesList = new ArrayList<>(); + + ArrayList mAddAnimations = new ArrayList<>(); + ArrayList mMoveAnimations = new ArrayList<>(); + ArrayList mRemoveAnimations = new ArrayList<>(); + ArrayList mChangeAnimations = new ArrayList<>(); + + private static class MoveInfo { + public RecyclerView.ViewHolder holder; + public int fromX, fromY, toX, toY; + + MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { + this.holder = holder; + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + } + + private static class ChangeInfo { + public RecyclerView.ViewHolder oldHolder, newHolder; + public int fromX, fromY, toX, toY; + private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) { + this.oldHolder = oldHolder; + this.newHolder = newHolder; + } + + ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + this(oldHolder, newHolder); + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public String toString() { + return "ChangeInfo{" + + "oldHolder=" + oldHolder + + ", newHolder=" + newHolder + + ", fromX=" + fromX + + ", fromY=" + fromY + + ", toX=" + toX + + ", toY=" + toY + + '}'; + } + } @Override public void runPendingAnimations() { @@ -69,7 +116,7 @@ public void runPendingAnimations() { mPendingRemovals.clear(); // Next, move stuff if (movesPending) { - ArrayList moves = new ArrayList<>(mPendingMoves); + final ArrayList moves = new ArrayList<>(mPendingMoves); mMovesList.add(moves); mPendingMoves.clear(); Runnable mover = () -> { @@ -89,7 +136,7 @@ public void runPendingAnimations() { } // Next, change stuff, to run in parallel with move animations if (changesPending) { - ArrayList changes = new ArrayList<>(mPendingChanges); + final ArrayList changes = new ArrayList<>(mPendingChanges); mChangesList.add(changes); mPendingChanges.clear(); Runnable changer = () -> { @@ -108,7 +155,7 @@ public void runPendingAnimations() { } // Next, add stuff if (additionsPending) { - ArrayList additions = new ArrayList<>(mPendingAdditions); + final ArrayList additions = new ArrayList<>(mPendingAdditions); mAdditionsList.add(additions); mPendingAdditions.clear(); Runnable adder = () -> { @@ -133,15 +180,15 @@ public void runPendingAnimations() { @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public boolean animateRemove(RecyclerView.ViewHolder holder) { + public boolean animateRemove(final RecyclerView.ViewHolder holder) { resetAnimation(holder); mPendingRemovals.add(holder); return true; } - private void animateRemoveImpl(RecyclerView.ViewHolder holder) { - View view = holder.itemView; - ViewPropertyAnimator animation = view.animate(); + private void animateRemoveImpl(final RecyclerView.ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimator animation = view.animate(); mRemoveAnimations.add(holder); animation.setDuration(getRemoveDuration()).alpha(0).setListener( new AnimatorListenerAdapter() { @@ -163,16 +210,16 @@ public void onAnimationEnd(Animator animator) { @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public boolean animateAdd(RecyclerView.ViewHolder holder) { + public boolean animateAdd(final RecyclerView.ViewHolder holder) { resetAnimation(holder); holder.itemView.setAlpha(0); mPendingAdditions.add(holder); return true; } - void animateAddImpl(RecyclerView.ViewHolder holder) { - View view = holder.itemView; - ViewPropertyAnimator animation = view.animate(); + void animateAddImpl(final RecyclerView.ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimator animation = view.animate(); mAddAnimations.add(holder); animation.alpha(1).setDuration(getAddDuration()) .setListener(new AnimatorListenerAdapter() { @@ -198,9 +245,9 @@ public void onAnimationEnd(Animator animator) { @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, - int toX, int toY) { - View view = holder.itemView; + public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, + int toX, int toY) { + final View view = holder.itemView; fromX += (int) holder.itemView.getTranslationX(); fromY += (int) holder.itemView.getTranslationY(); resetAnimation(holder); @@ -220,10 +267,10 @@ public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, return true; } - void animateMoveImpl(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { - View view = holder.itemView; - int deltaX = toX - fromX; - int deltaY = toY - fromY; + void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { + final View view = holder.itemView; + final int deltaX = toX - fromX; + final int deltaY = toY - fromY; if (deltaX != 0) { view.animate().translationX(0); } @@ -233,7 +280,7 @@ void animateMoveImpl(RecyclerView.ViewHolder holder, int fromX, int fromY, int t // TODO: make EndActions end listeners instead, since end actions aren't called when // vpas are canceled (and can't end them. why?) // need listener functionality in VPACompat for this. Ick. - ViewPropertyAnimator animation = view.animate(); + final ViewPropertyAnimator animation = view.animate(); mMoveAnimations.add(holder); animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() { @Override @@ -264,15 +311,15 @@ public void onAnimationEnd(Animator animator) { @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public boolean animateChange(RecyclerView.ViewHolder oldHolder, - RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) { + RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) { if (oldHolder == newHolder) { // Don't know how to run change animations when the same view holder is re-used. // run a move animation to handle position changes. return animateMove(oldHolder, fromLeft, fromTop, toLeft, toTop); } - float prevTranslationX = oldHolder.itemView.getTranslationX(); - float prevTranslationY = oldHolder.itemView.getTranslationY(); - float prevAlpha = oldHolder.itemView.getAlpha(); + final float prevTranslationX = oldHolder.itemView.getTranslationX(); + final float prevTranslationY = oldHolder.itemView.getTranslationY(); + final float prevAlpha = oldHolder.itemView.getAlpha(); resetAnimation(oldHolder); int deltaX = (int) (toLeft - fromLeft - prevTranslationX); int deltaY = (int) (toTop - fromTop - prevTranslationY); @@ -291,13 +338,13 @@ public boolean animateChange(RecyclerView.ViewHolder oldHolder, return true; } - void animateChangeImpl(ChangeInfo changeInfo) { - RecyclerView.ViewHolder holder = changeInfo.oldHolder; - View view = holder == null ? null : holder.itemView; - RecyclerView.ViewHolder newHolder = changeInfo.newHolder; - View newView = newHolder != null ? newHolder.itemView : null; + void animateChangeImpl(final ChangeInfo changeInfo) { + final RecyclerView.ViewHolder holder = changeInfo.oldHolder; + final View view = holder == null ? null : holder.itemView; + final RecyclerView.ViewHolder newHolder = changeInfo.newHolder; + final View newView = newHolder != null ? newHolder.itemView : null; if (view != null) { - ViewPropertyAnimator oldViewAnim = view.animate().setDuration( + final ViewPropertyAnimator oldViewAnim = view.animate().setDuration( getChangeDuration()); mChangeAnimations.add(changeInfo.oldHolder); oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); @@ -321,7 +368,7 @@ public void onAnimationEnd(Animator animator) { }).start(); } if (newView != null) { - ViewPropertyAnimator newViewAnimation = newView.animate(); + final ViewPropertyAnimator newViewAnimation = newView.animate(); mChangeAnimations.add(changeInfo.newHolder); newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()) .alpha(1).setListener(new AnimatorListenerAdapter() { @@ -329,7 +376,6 @@ public void onAnimationEnd(Animator animator) { public void onAnimationStart(Animator animator) { dispatchChangeStarting(changeInfo.newHolder, false); } - @Override public void onAnimationEnd(Animator animator) { newViewAnimation.setListener(null); @@ -363,7 +409,6 @@ private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); } } - private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) { boolean oldItem = false; if (changeInfo.newHolder == item) { @@ -384,7 +429,7 @@ private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerVie @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public void endAnimation(RecyclerView.ViewHolder item) { - View view = item.itemView; + final View view = item.itemView; // this will trigger end callback which should set properties to their target values. view.animate().cancel(); // TODO if some other animations are chained to end, how do we cancel them as well? @@ -442,21 +487,25 @@ public void endAnimation(RecyclerView.ViewHolder item) { } // animations should be ended by the cancel above. + //noinspection PointlessBooleanExpression,ConstantConditions if (mRemoveAnimations.remove(item) && DEBUG) { throw new IllegalStateException("after animation is cancelled, item should not be in " + "mRemoveAnimations list"); } + //noinspection PointlessBooleanExpression,ConstantConditions if (mAddAnimations.remove(item) && DEBUG) { throw new IllegalStateException("after animation is cancelled, item should not be in " + "mAddAnimations list"); } + //noinspection PointlessBooleanExpression,ConstantConditions if (mChangeAnimations.remove(item) && DEBUG) { throw new IllegalStateException("after animation is cancelled, item should not be in " + "mChangeAnimations list"); } + //noinspection PointlessBooleanExpression,ConstantConditions if (mMoveAnimations.remove(item) && DEBUG) { throw new IllegalStateException("after animation is cancelled, item should not be in " + "mMoveAnimations list"); @@ -607,55 +656,7 @@ void cancelAll(List viewHolders) { */ @Override public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, - @NonNull List payloads) { + @NonNull List payloads) { return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); } - - private static class MoveInfo { - public final RecyclerView.ViewHolder holder; - public final int fromX; - public final int fromY; - public final int toX; - public final int toY; - - MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { - this.holder = holder; - this.fromX = fromX; - this.fromY = fromY; - this.toX = toX; - this.toY = toY; - } - } - - private static class ChangeInfo { - public RecyclerView.ViewHolder oldHolder, newHolder; - public int fromX, fromY, toX, toY; - - private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) { - this.oldHolder = oldHolder; - this.newHolder = newHolder; - } - - ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, - int fromX, int fromY, int toX, int toY) { - this(oldHolder, newHolder); - this.fromX = fromX; - this.fromY = fromY; - this.toX = toX; - this.toY = toY; - } - - @Override - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public String toString() { - return "ChangeInfo{" - + "oldHolder=" + oldHolder - + ", newHolder=" + newHolder - + ", fromX=" + fromX - + ", fromY=" + fromY - + ", toX=" + toX - + ", toY=" + toY - + '}'; - } - } } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/DiffUtil.java b/viewpager2/src/main/java/androidx/recyclerview/widget/DiffUtil.java index 1220ea987..676752dd3 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/DiffUtil.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/DiffUtil.java @@ -80,12 +80,12 @@ * @see AsyncListDiffer */ public class DiffUtil { - private static final Comparator DIAGONAL_COMPARATOR = (o1, o2) -> o1.x - o2.x; - private DiffUtil() { // utility class, no instance. } + private static final Comparator DIAGONAL_COMPARATOR = (o1, o2) -> o1.x - o2.x; + // Myers' algorithm uses two lists as axis labels. In DiffUtil's implementation, `x` axis is // used for old list and `y` axis is used for new list. @@ -108,43 +108,44 @@ public static DiffResult calculateDiff(@NonNull Callback cb) { * positions), you can disable move detection which takes O(N^2) time where * N is the number of added, moved, removed items. * - * @param cb The callback that acts as a gateway to the backing list data + * @param cb The callback that acts as a gateway to the backing list data * @param detectMoves True if DiffUtil should try to detect moved items, false otherwise. + * * @return A DiffResult that contains the information about the edit sequence to convert the * old list into the new list. */ @NonNull public static DiffResult calculateDiff(@NonNull Callback cb, boolean detectMoves) { - int oldSize = cb.getOldListSize(); - int newSize = cb.getNewListSize(); + final int oldSize = cb.getOldListSize(); + final int newSize = cb.getNewListSize(); - List diagonals = new ArrayList<>(); + final List diagonals = new ArrayList<>(); // instead of a recursive implementation, we keep our own stack to avoid potential stack // overflow exceptions - List stack = new ArrayList<>(); + final List stack = new ArrayList<>(); stack.add(new Range(0, oldSize, 0, newSize)); - int max = (oldSize + newSize + 1) / 2; + final int max = (oldSize + newSize + 1) / 2; // allocate forward and backward k-lines. K lines are diagonal lines in the matrix. (see the // paper for details) // These arrays lines keep the max reachable position for each k-line. - CenteredArray forward = new CenteredArray(max * 2 + 1); - CenteredArray backward = new CenteredArray(max * 2 + 1); + final CenteredArray forward = new CenteredArray(max * 2 + 1); + final CenteredArray backward = new CenteredArray(max * 2 + 1); // We pool the ranges to avoid allocations for each recursive call. - List rangePool = new ArrayList<>(); + final List rangePool = new ArrayList<>(); while (!stack.isEmpty()) { - Range range = stack.remove(stack.size() - 1); - Snake snake = midPoint(range, cb, forward, backward); + final Range range = stack.remove(stack.size() - 1); + final Snake snake = midPoint(range, cb, forward, backward); if (snake != null) { // if it has a diagonal, save it if (snake.diagonalSize() > 0) { diagonals.add(snake.toDiagonal()); } // add new ranges for left and right - Range left = rangePool.isEmpty() ? new Range() : rangePool.remove( + final Range left = rangePool.isEmpty() ? new Range() : rangePool.remove( rangePool.size() - 1); left.oldListStart = range.oldListStart; left.newListStart = range.newListStart; @@ -154,7 +155,7 @@ public static DiffResult calculateDiff(@NonNull Callback cb, boolean detectMoves // re-use range for right //noinspection UnnecessaryLocalVariable - Range right = range; + final Range right = range; right.oldListEnd = range.oldListEnd; right.newListEnd = range.newListEnd; right.oldListStart = snake.endX; @@ -214,8 +215,8 @@ private static Snake forward( // we either come from d-1, k-1 OR d-1. k+1 // as we move in steps of 2, array always holds both current and previous d values // k = x - y and each array value holds the max X, y = x - k - int startX; - int startY; + final int startX; + final int startY; int x, y; if (k == -d || (k != d && forward.get(k + 1) > forward.get(k - 1))) { // picking k + 1, incrementing Y (by simply not incrementing X) @@ -274,8 +275,8 @@ private static Snake backward( // as we move in steps of 2, array always holds both current and previous d values // k = x - y and each array value holds the MIN X, y = x - k // when x's are equal, we prioritize deletion over insertion - int startX; - int startY; + final int startX; + final int startY; int x, y; if (k == -d || (k != d && backward.get(k + 1) < backward.get(k - 1))) { @@ -456,7 +457,7 @@ public abstract static class ItemCallback { * * @see Callback#getChangePayload(int, int) */ - @SuppressWarnings("unused") + @SuppressWarnings({"unused"}) @Nullable public Object getChangePayload(@NonNull T oldItem, @NonNull T newItem) { return null; @@ -655,7 +656,7 @@ public static class DiffResult { * @param detectMoves True if this DiffResult will try to detect moved items */ DiffResult(Callback callback, List diagonals, int[] oldItemStatuses, - int[] newItemStatuses, boolean detectMoves) { + int[] newItemStatuses, boolean detectMoves) { mDiagonals = diagonals; mOldItemStatuses = oldItemStatuses; mNewItemStatuses = newItemStatuses; @@ -669,33 +670,6 @@ public static class DiffResult { findMatchingItems(); } - @Nullable - private static PostponedUpdate getPostponedUpdate( - Collection postponedUpdates, - int posInList, - boolean removal) { - PostponedUpdate postponedUpdate = null; - Iterator itr = postponedUpdates.iterator(); - while (itr.hasNext()) { - PostponedUpdate update = itr.next(); - if (update.posInOwnerList == posInList && update.removal == removal) { - postponedUpdate = update; - itr.remove(); - break; - } - } - while (itr.hasNext()) { - // re-offset all others - PostponedUpdate update = itr.next(); - if (removal) { - update.currentPos--; - } else { - update.currentPos++; - } - } - return postponedUpdate; - } - /** * Add edge diagonals so that we can iterate as long as there are diagonals w/o lots of * null checks around @@ -720,8 +694,8 @@ private void findMatchingItems() { for (int offset = 0; offset < diagonal.size; offset++) { int posX = diagonal.x + offset; int posY = diagonal.y + offset; - boolean theSame = mCallback.areContentsTheSame(posX, posY); - int changeFlag = theSame ? FLAG_NOT_CHANGED : FLAG_CHANGED; + final boolean theSame = mCallback.areContentsTheSame(posX, posY); + final int changeFlag = theSame ? FLAG_NOT_CHANGED : FLAG_CHANGED; mOldItemStatuses[posX] = (posY << FLAG_OFFSET) | changeFlag; mNewItemStatuses[posY] = (posX << FLAG_OFFSET) | changeFlag; } @@ -757,9 +731,9 @@ private void findMoveMatches() { */ private void findMatchingAddition(int posX) { int posY = 0; - int diagonalsSize = mDiagonals.size(); + final int diagonalsSize = mDiagonals.size(); for (int i = 0; i < diagonalsSize; i++) { - Diagonal diagonal = mDiagonals.get(i); + final Diagonal diagonal = mDiagonals.get(i); while (posY < diagonal.y) { // found some additions, evaluate if (mNewItemStatuses[posY] == 0) { // not evaluated yet @@ -767,7 +741,7 @@ private void findMatchingAddition(int posX) { if (matching) { // yay found it, set values boolean contentsMatching = mCallback.areContentsTheSame(posX, posY); - int changeFlag = contentsMatching ? FLAG_MOVED_NOT_CHANGED + final int changeFlag = contentsMatching ? FLAG_MOVED_NOT_CHANGED : FLAG_MOVED_CHANGED; // once we process one of these, it will mark the other one as ignored. mOldItemStatuses[posX] = (posY << FLAG_OFFSET) | changeFlag; @@ -795,7 +769,7 @@ public int convertOldPositionToNew(@IntRange(from = 0) int oldListPosition) { throw new IndexOutOfBoundsException("Index out of bounds - passed position = " + oldListPosition + ", old list size = " + mOldListSize); } - int status = mOldItemStatuses[oldListPosition]; + final int status = mOldItemStatuses[oldListPosition]; if ((status & FLAG_MASK) == 0) { return NO_POSITION; } else { @@ -817,7 +791,7 @@ public int convertNewPositionToOld(@IntRange(from = 0) int newListPosition) { throw new IndexOutOfBoundsException("Index out of bounds - passed position = " + newListPosition + ", new list size = " + mNewListSize); } - int status = mNewItemStatuses[newListPosition]; + final int status = mNewItemStatuses[newListPosition]; if ((status & FLAG_MASK) == 0) { return NO_POSITION; } else { @@ -857,7 +831,7 @@ public int convertNewPositionToOld(@IntRange(from = 0) int newListPosition) { * displaying the new list. * @see AdapterListUpdateCallback */ - public void dispatchUpdatesTo(@NonNull RecyclerView.Adapter adapter) { + public void dispatchUpdatesTo(@NonNull final RecyclerView.Adapter adapter) { dispatchUpdatesTo(new AdapterListUpdateCallback(adapter)); } @@ -871,7 +845,7 @@ public void dispatchUpdatesTo(@NonNull RecyclerView.Adapter adapter) { * @see #dispatchUpdatesTo(RecyclerView.Adapter) */ public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) { - BatchingListUpdateCallback batchingCallback; + final BatchingListUpdateCallback batchingCallback; if (updateCallback instanceof BatchingListUpdateCallback) { batchingCallback = (BatchingListUpdateCallback) updateCallback; @@ -888,7 +862,7 @@ public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) { // Later when we find the match of that move, we dispatch the update int currentListSize = mOldListSize; // list of postponed moves - Collection postponedUpdates = new ArrayDeque<>(); + final Collection postponedUpdates = new ArrayDeque<>(); // posX and posY are exclusive int posX = mOldListSize; int posY = mNewListSize; @@ -896,7 +870,7 @@ public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) { // this just makes offsets easier since changes in the earlier indices has an effect // on the later indices. for (int diagonalIndex = mDiagonals.size() - 1; diagonalIndex >= 0; diagonalIndex--) { - Diagonal diagonal = mDiagonals.get(diagonalIndex); + final Diagonal diagonal = mDiagonals.get(diagonalIndex); int endX = diagonal.endX(); int endY = diagonal.endY(); // dispatch removals and additions until we reach to that diagonal @@ -987,6 +961,33 @@ public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) { } batchingCallback.dispatchLastEvent(); } + + @Nullable + private static PostponedUpdate getPostponedUpdate( + Collection postponedUpdates, + int posInList, + boolean removal) { + PostponedUpdate postponedUpdate = null; + Iterator itr = postponedUpdates.iterator(); + while (itr.hasNext()) { + PostponedUpdate update = itr.next(); + if (update.posInOwnerList == posInList && update.removal == removal) { + postponedUpdate = update; + itr.remove(); + break; + } + } + while (itr.hasNext()) { + // re-offset all others + PostponedUpdate update = itr.next(); + if (removal) { + update.currentPos--; + } else { + update.currentPos++; + } + } + return postponedUpdate; + } } /** @@ -1000,16 +1001,18 @@ private static class PostponedUpdate { /** * position in the list that owns this item */ - final int posInOwnerList; - /** - * true if this is a removal, false otherwise - */ - final boolean removal; + int posInOwnerList; + /** * position wrt to the end of the list */ int currentPos; + /** + * true if this is a removal, false otherwise + */ + boolean removal; + PostponedUpdate(int posInOwnerList, int currentPos, boolean removal) { this.posInOwnerList = posInOwnerList; this.currentPos = currentPos; diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/DividerItemDecoration.java b/viewpager2/src/main/java/androidx/recyclerview/widget/DividerItemDecoration.java index ac88b9446..b4598edfe 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/DividerItemDecoration.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/DividerItemDecoration.java @@ -46,24 +46,27 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration { public static final int VERTICAL = LinearLayout.VERTICAL; private static final String TAG = "DividerItem"; - private static final int[] ATTRS = {android.R.attr.listDivider}; - private final Rect mBounds = new Rect(); + private static final int[] ATTRS = new int[]{ android.R.attr.listDivider }; + private Drawable mDivider; + /** * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}. */ private int mOrientation; + private final Rect mBounds = new Rect(); + /** * Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a * {@link LinearLayoutManager}. * - * @param context Current context, it will be used to access resources. + * @param context Current context, it will be used to access resources. * @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}. */ @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public DividerItemDecoration(Context context, int orientation) { - @SuppressLint("ResourceType") TypedArray a = context.obtainStyledAttributes(ATTRS); + final TypedArray a = context.obtainStyledAttributes(ATTRS); mDivider = a.getDrawable(0); if (mDivider == null) { Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this " @@ -87,14 +90,6 @@ public void setOrientation(int orientation) { mOrientation = orientation; } - /** - * @return the {@link Drawable} for this divider. - */ - @Nullable - public Drawable getDrawable() { - return mDivider; - } - /** * Sets the {@link Drawable} for this divider. * @@ -107,6 +102,14 @@ public void setDrawable(@NonNull Drawable drawable) { mDivider = drawable; } + /** + * @return the {@link Drawable} for this divider. + */ + @Nullable + public Drawable getDrawable() { + return mDivider; + } + @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { @@ -122,8 +125,8 @@ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { private void drawVertical(Canvas canvas, RecyclerView parent) { canvas.save(); - int left; - int right; + final int left; + final int right; //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides. if (parent.getClipToPadding()) { left = parent.getPaddingLeft(); @@ -135,12 +138,12 @@ private void drawVertical(Canvas canvas, RecyclerView parent) { right = parent.getWidth(); } - int childCount = parent.getChildCount(); + final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { - View child = parent.getChildAt(i); + final View child = parent.getChildAt(i); parent.getDecoratedBoundsWithMargins(child, mBounds); - int bottom = mBounds.bottom + Math.round(child.getTranslationY()); - int top = bottom - mDivider.getIntrinsicHeight(); + final int bottom = mBounds.bottom + Math.round(child.getTranslationY()); + final int top = bottom - mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(canvas); } @@ -149,8 +152,8 @@ private void drawVertical(Canvas canvas, RecyclerView parent) { private void drawHorizontal(Canvas canvas, RecyclerView parent) { canvas.save(); - int top; - int bottom; + final int top; + final int bottom; //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides. if (parent.getClipToPadding()) { top = parent.getPaddingTop(); @@ -162,12 +165,12 @@ private void drawHorizontal(Canvas canvas, RecyclerView parent) { bottom = parent.getHeight(); } - int childCount = parent.getChildCount(); + final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { - View child = parent.getChildAt(i); + final View child = parent.getChildAt(i); parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds); - int right = mBounds.right + Math.round(child.getTranslationX()); - int left = right - mDivider.getIntrinsicWidth(); + final int right = mBounds.right + Math.round(child.getTranslationX()); + final int left = right - mDivider.getIntrinsicWidth(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(canvas); } @@ -177,7 +180,7 @@ private void drawHorizontal(Canvas canvas, RecyclerView parent) { @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public void getItemOffsets(Rect outRect, View view, RecyclerView parent, - RecyclerView.State state) { + RecyclerView.State state) { if (mDivider == null) { outRect.set(0, 0, 0, 0); return; diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/FastScroller.java b/viewpager2/src/main/java/androidx/recyclerview/widget/FastScroller.java index 72d3cd510..f93b1c8fd 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/FastScroller.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/FastScroller.java @@ -39,74 +39,88 @@ */ @VisibleForTesting class FastScroller extends RecyclerView.ItemDecoration implements RecyclerView.OnItemTouchListener { + @IntDef({STATE_HIDDEN, STATE_VISIBLE, STATE_DRAGGING}) + @Retention(RetentionPolicy.SOURCE) + private @interface State { } // Scroll thumb not showing private static final int STATE_HIDDEN = 0; // Scroll thumb visible and moving along with the scrollbar private static final int STATE_VISIBLE = 1; // Scroll thumb being dragged by user private static final int STATE_DRAGGING = 2; + + @IntDef({DRAG_X, DRAG_Y, DRAG_NONE}) + @Retention(RetentionPolicy.SOURCE) + private @interface DragState{ } private static final int DRAG_NONE = 0; private static final int DRAG_X = 1; private static final int DRAG_Y = 2; + + @IntDef({ANIMATION_STATE_OUT, ANIMATION_STATE_FADING_IN, ANIMATION_STATE_IN, + ANIMATION_STATE_FADING_OUT}) + @Retention(RetentionPolicy.SOURCE) + private @interface AnimationState { } private static final int ANIMATION_STATE_OUT = 0; private static final int ANIMATION_STATE_FADING_IN = 1; private static final int ANIMATION_STATE_IN = 2; private static final int ANIMATION_STATE_FADING_OUT = 3; + private static final int SHOW_DURATION_MS = 500; private static final int HIDE_DELAY_AFTER_VISIBLE_MS = 1500; private static final int HIDE_DELAY_AFTER_DRAGGING_MS = 1200; private static final int HIDE_DURATION_MS = 500; private static final int SCROLLBAR_FULL_OPAQUE = 255; - private static final int[] PRESSED_STATE_SET = {android.R.attr.state_pressed}; - private static final int[] EMPTY_STATE_SET = {}; + + private static final int[] PRESSED_STATE_SET = new int[]{android.R.attr.state_pressed}; + private static final int[] EMPTY_STATE_SET = new int[]{}; + + private final int mScrollbarMinimumRange; + private final int mMargin; + // Final values for the vertical scroll bar @SuppressWarnings("WeakerAccess") /* synthetic access */ final StateListDrawable mVerticalThumbDrawable; @SuppressWarnings("WeakerAccess") /* synthetic access */ final Drawable mVerticalTrackDrawable; - @SuppressWarnings("WeakerAccess") /* synthetic access */ - final ValueAnimator mShowHideAnimator = ValueAnimator.ofFloat(0, 1); - private final int mScrollbarMinimumRange; - private final int mMargin; private final int mVerticalThumbWidth; private final int mVerticalTrackWidth; + // Final values for the horizontal scroll bar private final StateListDrawable mHorizontalThumbDrawable; private final Drawable mHorizontalTrackDrawable; private final int mHorizontalThumbHeight; private final int mHorizontalTrackHeight; - private final int[] mVerticalRange = new int[2]; - private final int[] mHorizontalRange = new int[2]; + // Dynamic values for the vertical scroll bar - @VisibleForTesting - int mVerticalThumbHeight; - @VisibleForTesting - int mVerticalThumbCenterY; - @VisibleForTesting - float mVerticalDragY; + @VisibleForTesting int mVerticalThumbHeight; + @VisibleForTesting int mVerticalThumbCenterY; + @VisibleForTesting float mVerticalDragY; // Dynamic values for the horizontal scroll bar - @VisibleForTesting - int mHorizontalThumbWidth; - @VisibleForTesting - int mHorizontalThumbCenterX; - @VisibleForTesting - float mHorizontalDragX; - @SuppressWarnings("WeakerAccess") /* synthetic access */ - @AnimationState - int mAnimationState = ANIMATION_STATE_OUT; - private final Runnable mHideRunnable = () -> hide(HIDE_DURATION_MS); - private int mRecyclerViewWidth; - private int mRecyclerViewHeight; + @VisibleForTesting int mHorizontalThumbWidth; + @VisibleForTesting int mHorizontalThumbCenterX; + @VisibleForTesting float mHorizontalDragX; + + private int mRecyclerViewWidth = 0; + private int mRecyclerViewHeight = 0; + private RecyclerView mRecyclerView; /** * Whether the document is long/wide enough to require scrolling. If not, we don't show the * relevant scroller. */ - private boolean mNeedVerticalScrollbar; - private boolean mNeedHorizontalScrollbar; - @State - private int mState = STATE_HIDDEN; + private boolean mNeedVerticalScrollbar = false; + private boolean mNeedHorizontalScrollbar = false; + @State private int mState = STATE_HIDDEN; + @DragState private int mDragState = DRAG_NONE; + + private final int[] mVerticalRange = new int[2]; + private final int[] mHorizontalRange = new int[2]; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final ValueAnimator mShowHideAnimator = ValueAnimator.ofFloat(0, 1); + @SuppressWarnings("WeakerAccess") /* synthetic access */ + @AnimationState int mAnimationState = ANIMATION_STATE_OUT; + private final Runnable mHideRunnable = () -> hide(HIDE_DURATION_MS); private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() { @Override @@ -115,13 +129,11 @@ public void onScrolled(RecyclerView recyclerView, int dx, int dy) { recyclerView.computeVerticalScrollOffset()); } }; - @DragState - private int mDragState = DRAG_NONE; FastScroller(RecyclerView recyclerView, StateListDrawable verticalThumbDrawable, - Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable, - Drawable horizontalTrackDrawable, int defaultWidth, int scrollbarMinimumRange, - int margin) { + Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable, + Drawable horizontalTrackDrawable, int defaultWidth, int scrollbarMinimumRange, + int margin) { mVerticalThumbDrawable = verticalThumbDrawable; mVerticalTrackDrawable = verticalTrackDrawable; mHorizontalThumbDrawable = horizontalThumbDrawable; @@ -129,9 +141,9 @@ public void onScrolled(RecyclerView recyclerView, int dx, int dy) { mVerticalThumbWidth = Math.max(defaultWidth, verticalThumbDrawable.getIntrinsicWidth()); mVerticalTrackWidth = Math.max(defaultWidth, verticalTrackDrawable.getIntrinsicWidth()); mHorizontalThumbHeight = Math - .max(defaultWidth, horizontalThumbDrawable.getIntrinsicWidth()); + .max(defaultWidth, horizontalThumbDrawable.getIntrinsicWidth()); mHorizontalTrackHeight = Math - .max(defaultWidth, horizontalTrackDrawable.getIntrinsicWidth()); + .max(defaultWidth, horizontalTrackDrawable.getIntrinsicWidth()); mScrollbarMinimumRange = scrollbarMinimumRange; mMargin = margin; mVerticalThumbDrawable.setAlpha(SCROLLBAR_FULL_OPAQUE); @@ -203,8 +215,7 @@ public boolean isDragging() { return mState == STATE_DRAGGING; } - @VisibleForTesting - boolean isVisible() { + @VisibleForTesting boolean isVisible() { return mState == STATE_VISIBLE; } @@ -248,7 +259,7 @@ private void resetHideDelay(int delay) { } @Override - public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) { if (mRecyclerViewWidth != mRecyclerView.getWidth() || mRecyclerViewHeight != mRecyclerView.getHeight()) { mRecyclerViewWidth = mRecyclerView.getWidth(); @@ -278,7 +289,7 @@ private void drawVerticalScrollbar(Canvas canvas) { int top = mVerticalThumbCenterY - mVerticalThumbHeight / 2; mVerticalThumbDrawable.setBounds(0, 0, mVerticalThumbWidth, mVerticalThumbHeight); mVerticalTrackDrawable - .setBounds(0, 0, mVerticalTrackWidth, mRecyclerViewHeight); + .setBounds(0, 0, mVerticalTrackWidth, mRecyclerViewHeight); if (isLayoutRTL()) { mVerticalTrackDrawable.draw(canvas); @@ -303,7 +314,7 @@ private void drawHorizontalScrollbar(Canvas canvas) { int left = mHorizontalThumbCenterX - mHorizontalThumbWidth / 2; mHorizontalThumbDrawable.setBounds(0, 0, mHorizontalThumbWidth, mHorizontalThumbHeight); mHorizontalTrackDrawable - .setBounds(0, 0, mRecyclerViewWidth, mHorizontalTrackHeight); + .setBounds(0, 0, mRecyclerViewWidth, mHorizontalTrackHeight); canvas.translate(0, top); mHorizontalTrackDrawable.draw(canvas); @@ -323,12 +334,12 @@ void updateScrollPosition(int offsetX, int offsetY) { int verticalContentLength = mRecyclerView.computeVerticalScrollRange(); int verticalVisibleLength = mRecyclerViewHeight; mNeedVerticalScrollbar = verticalContentLength - verticalVisibleLength > 0 - && mRecyclerViewHeight >= mScrollbarMinimumRange; + && mRecyclerViewHeight >= mScrollbarMinimumRange; int horizontalContentLength = mRecyclerView.computeHorizontalScrollRange(); int horizontalVisibleLength = mRecyclerViewWidth; mNeedHorizontalScrollbar = horizontalContentLength - horizontalVisibleLength > 0 - && mRecyclerViewWidth >= mScrollbarMinimumRange; + && mRecyclerViewWidth >= mScrollbarMinimumRange; if (!mNeedVerticalScrollbar && !mNeedHorizontalScrollbar) { if (mState != STATE_HIDDEN) { @@ -340,17 +351,17 @@ void updateScrollPosition(int offsetX, int offsetY) { if (mNeedVerticalScrollbar) { float middleScreenPos = offsetY + verticalVisibleLength / 2.0f; mVerticalThumbCenterY = - (int) ((verticalVisibleLength * middleScreenPos) / verticalContentLength); + (int) ((verticalVisibleLength * middleScreenPos) / verticalContentLength); mVerticalThumbHeight = Math.min(verticalVisibleLength, - (verticalVisibleLength * verticalVisibleLength) / verticalContentLength); + (verticalVisibleLength * verticalVisibleLength) / verticalContentLength); } if (mNeedHorizontalScrollbar) { float middleScreenPos = offsetX + horizontalVisibleLength / 2.0f; mHorizontalThumbCenterX = - (int) ((horizontalVisibleLength * middleScreenPos) / horizontalContentLength); + (int) ((horizontalVisibleLength * middleScreenPos) / horizontalContentLength); mHorizontalThumbWidth = Math.min(horizontalVisibleLength, - (horizontalVisibleLength * horizontalVisibleLength) / horizontalContentLength); + (horizontalVisibleLength * horizontalVisibleLength) / horizontalContentLength); } if (mState == STATE_HIDDEN || mState == STATE_VISIBLE) { @@ -360,8 +371,8 @@ void updateScrollPosition(int offsetX, int offsetY) { @Override public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, - @NonNull MotionEvent ev) { - boolean handled; + @NonNull MotionEvent ev) { + final boolean handled; if (mState == STATE_VISIBLE) { boolean insideVerticalThumb = isPointInsideVerticalThumb(ev.getX(), ev.getY()); boolean insideHorizontalThumb = isPointInsideHorizontalThumb(ev.getX(), ev.getY()); @@ -380,7 +391,11 @@ public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, } else { handled = false; } - } else handled = mState == STATE_DRAGGING; + } else if (mState == STATE_DRAGGING) { + handled = true; + } else { + handled = false; + } return handled; } @@ -420,11 +435,10 @@ public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEven } @Override - public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { - } + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } private void verticalScrollTo(float y) { - int[] scrollbarRange = getVerticalRange(); + final int[] scrollbarRange = getVerticalRange(); y = Math.max(scrollbarRange[0], Math.min(scrollbarRange[1], y)); if (Math.abs(mVerticalThumbCenterY - y) < 2) { return; @@ -439,7 +453,7 @@ private void verticalScrollTo(float y) { } private void horizontalScrollTo(float x) { - int[] scrollbarRange = getHorizontalRange(); + final int[] scrollbarRange = getHorizontalRange(); x = Math.max(scrollbarRange[0], Math.min(scrollbarRange[1], x)); if (Math.abs(mHorizontalThumbCenterX - x) < 2) { return; @@ -456,7 +470,7 @@ private void horizontalScrollTo(float x) { } private int scrollTo(float oldDragPos, float newDragPos, int[] scrollbarRange, int scrollRange, - int scrollOffset, int viewLength) { + int scrollOffset, int viewLength) { int scrollbarLength = scrollbarRange[1] - scrollbarRange[0]; if (scrollbarLength == 0) { return 0; @@ -475,16 +489,16 @@ private int scrollTo(float oldDragPos, float newDragPos, int[] scrollbarRange, i @VisibleForTesting boolean isPointInsideVerticalThumb(float x, float y) { return (isLayoutRTL() ? x <= mVerticalThumbWidth - : x >= mRecyclerViewWidth - mVerticalThumbWidth) - && y >= mVerticalThumbCenterY - mVerticalThumbHeight / 2 - && y <= mVerticalThumbCenterY + mVerticalThumbHeight / 2; + : x >= mRecyclerViewWidth - mVerticalThumbWidth) + && y >= mVerticalThumbCenterY - mVerticalThumbHeight / 2 + && y <= mVerticalThumbCenterY + mVerticalThumbHeight / 2; } @VisibleForTesting boolean isPointInsideHorizontalThumb(float x, float y) { return (y >= mRecyclerViewHeight - mHorizontalThumbHeight) - && x >= mHorizontalThumbCenterX - mHorizontalThumbWidth / 2 - && x <= mHorizontalThumbCenterX + mHorizontalThumbWidth / 2; + && x >= mHorizontalThumbCenterX - mHorizontalThumbWidth / 2 + && x <= mHorizontalThumbCenterX + mHorizontalThumbWidth / 2; } @VisibleForTesting @@ -525,25 +539,9 @@ private int[] getHorizontalRange() { return mHorizontalRange; } - @IntDef({STATE_HIDDEN, STATE_VISIBLE, STATE_DRAGGING}) - @Retention(RetentionPolicy.SOURCE) - private @interface State { - } - - @IntDef({DRAG_X, DRAG_Y, DRAG_NONE}) - @Retention(RetentionPolicy.SOURCE) - private @interface DragState { - } - - @IntDef({ANIMATION_STATE_OUT, ANIMATION_STATE_FADING_IN, ANIMATION_STATE_IN, - ANIMATION_STATE_FADING_OUT}) - @Retention(RetentionPolicy.SOURCE) - private @interface AnimationState { - } - private class AnimatorListener extends AnimatorListenerAdapter { - private boolean mCanceled; + private boolean mCanceled = false; AnimatorListener() { } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/GapWorker.java b/viewpager2/src/main/java/androidx/recyclerview/widget/GapWorker.java index 66823da9e..8c44cf09e 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/GapWorker.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/GapWorker.java @@ -30,45 +30,130 @@ final class GapWorker implements Runnable { static final ThreadLocal sGapWorker = new ThreadLocal<>(); - static final Comparator sTaskComparator = (lhs, rhs) -> { - // first, prioritize non-cleared tasks - if ((lhs.view == null) != (rhs.view == null)) { - return lhs.view == null ? 1 : -1; - } - // then prioritize immediate - if (lhs.immediate != rhs.immediate) { - return lhs.immediate ? -1 : 1; - } + ArrayList mRecyclerViews = new ArrayList<>(); + long mPostTimeNs; + long mFrameIntervalNs; - // then prioritize _highest_ view velocity - int deltaViewVelocity = rhs.viewVelocity - lhs.viewVelocity; - if (deltaViewVelocity != 0) return deltaViewVelocity; + static class Task { + public boolean immediate; + public int viewVelocity; + public int distanceToItem; + public RecyclerView view; + public int position; + + public void clear() { + immediate = false; + viewVelocity = 0; + distanceToItem = 0; + view = null; + position = 0; + } + } - // then prioritize _lowest_ distance to item - return lhs.distanceToItem - rhs.distanceToItem; - }; - final ArrayList mRecyclerViews = new ArrayList<>(); /** * Temporary storage for prefetch Tasks that execute in {@link #prefetch(long)}. Task objects * are pooled in the ArrayList, and never removed to avoid allocations, but always cleared * in between calls. */ - private final ArrayList mTasks = new ArrayList<>(); - long mPostTimeNs; - long mFrameIntervalNs; + private ArrayList mTasks = new ArrayList<>(); - static boolean isPrefetchPositionAttached(RecyclerView view, int position) { - int childCount = view.mChildHelper.getUnfilteredChildCount(); - for (int i = 0; i < childCount; i++) { - View attachedView = view.mChildHelper.getUnfilteredChildAt(i); - RecyclerView.ViewHolder holder = RecyclerView.getChildViewHolderInt(attachedView); - // Note: can use mPosition here because adapter doesn't have pending updates - if (holder.mPosition == position && !holder.isInvalid()) { - return true; + /** + * Prefetch information associated with a specific RecyclerView. + */ + @SuppressLint("VisibleForTests") + static class LayoutPrefetchRegistryImpl + implements RecyclerView.LayoutManager.LayoutPrefetchRegistry { + int mPrefetchDx; + int mPrefetchDy; + int[] mPrefetchArray; + + int mCount; + + void setPrefetchVector(int dx, int dy) { + mPrefetchDx = dx; + mPrefetchDy = dy; + } + + void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) { + mCount = 0; + if (mPrefetchArray != null) { + Arrays.fill(mPrefetchArray, -1); + } + + final RecyclerView.LayoutManager layout = view.mLayout; + if (view.mAdapter != null + && layout != null + && layout.isItemPrefetchEnabled()) { + if (nested) { + // nested prefetch, only if no adapter updates pending. Note: we don't query + // view.hasPendingAdapterUpdates(), as first layout may not have occurred + if (!view.mAdapterHelper.hasPendingUpdates()) { + layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this); + } + } else { + // momentum based prefetch, only if we trust current child/adapter state + if (!view.hasPendingAdapterUpdates()) { + layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy, + view.mState, this); + } + } + + if (mCount > layout.mPrefetchMaxCountObserved) { + layout.mPrefetchMaxCountObserved = mCount; + layout.mPrefetchMaxObservedInInitialPrefetch = nested; + view.mRecycler.updateViewCacheSize(); + } } } - return false; + + @Override + public void addPosition(int layoutPosition, int pixelDistance) { + if (layoutPosition < 0) { + throw new IllegalArgumentException("Layout positions must be non-negative"); + } + + if (pixelDistance < 0) { + throw new IllegalArgumentException("Pixel distance must be non-negative"); + } + + // allocate or expand array as needed, doubling when needed + final int storagePosition = mCount * 2; + if (mPrefetchArray == null) { + mPrefetchArray = new int[4]; + Arrays.fill(mPrefetchArray, -1); + } else if (storagePosition >= mPrefetchArray.length) { + final int[] oldArray = mPrefetchArray; + mPrefetchArray = new int[storagePosition * 2]; + System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length); + } + + // add position + mPrefetchArray[storagePosition] = layoutPosition; + mPrefetchArray[storagePosition + 1] = pixelDistance; + + mCount++; + } + + boolean lastPrefetchIncludedPosition(int position) { + if (mPrefetchArray != null) { + final int count = mCount * 2; + for (int i = 0; i < count; i += 2) { + if (mPrefetchArray[i] == position) return true; + } + } + return false; + } + + /** + * Called when prefetch indices are no longer valid for cache prioritization. + */ + void clearPrefetchPositions() { + if (mPrefetchArray != null) { + Arrays.fill(mPrefetchArray, -1); + } + mCount = 0; + } } public void add(RecyclerView recyclerView) { @@ -102,9 +187,31 @@ void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy); } + static Comparator sTaskComparator = (lhs, rhs) -> { + // first, prioritize non-cleared tasks + if ((lhs.view == null) != (rhs.view == null)) { + return lhs.view == null ? 1 : -1; + } + + // then prioritize immediate + if (lhs.immediate != rhs.immediate) { + return lhs.immediate ? -1 : 1; + } + + // then prioritize _highest_ view velocity + int deltaViewVelocity = rhs.viewVelocity - lhs.viewVelocity; + if (deltaViewVelocity != 0) return deltaViewVelocity; + + // then prioritize _lowest_ distance to item + int deltaDistanceToItem = lhs.distanceToItem - rhs.distanceToItem; + if (deltaDistanceToItem != 0) return deltaDistanceToItem; + + return 0; + }; + private void buildTaskList() { // Update PrefetchRegistry in each view - int viewCount = mRecyclerViews.size(); + final int viewCount = mRecyclerViews.size(); int totalTaskCount = 0; for (int i = 0; i < viewCount; i++) { RecyclerView view = mRecyclerViews.get(i); @@ -125,17 +232,17 @@ private void buildTaskList() { } LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry; - int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx) + final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx) + Math.abs(prefetchRegistry.mPrefetchDy); for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) { - Task task; + final Task task; if (totalTaskIndex >= mTasks.size()) { task = new Task(); mTasks.add(task); } else { task = mTasks.get(totalTaskIndex); } - int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1]; + final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1]; task.immediate = distanceToItem <= viewVelocity; task.viewVelocity = viewVelocity; @@ -151,8 +258,21 @@ private void buildTaskList() { Collections.sort(mTasks, sTaskComparator); } + static boolean isPrefetchPositionAttached(RecyclerView view, int position) { + final int childCount = view.mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + View attachedView = view.mChildHelper.getUnfilteredChildAt(i); + RecyclerView.ViewHolder holder = RecyclerView.getChildViewHolderInt(attachedView); + // Note: can use mPosition here because adapter doesn't have pending updates + if (holder.mPosition == position && !holder.isInvalid()) { + return true; + } + } + return false; + } + private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view, - int position, long deadlineNs) { + int position, long deadlineNs) { if (isPrefetchPositionAttached(view, position)) { // don't attempt to prefetch attached views return null; @@ -185,7 +305,7 @@ private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view, } private void prefetchInnerRecyclerViewWithDeadline(@Nullable RecyclerView innerView, - long deadlineNs) { + long deadlineNs) { if (innerView == null) { return; } @@ -198,7 +318,7 @@ private void prefetchInnerRecyclerViewWithDeadline(@Nullable RecyclerView innerV } // do nested prefetch! - LayoutPrefetchRegistryImpl innerPrefetchRegistry = innerView.mPrefetchRegistry; + final LayoutPrefetchRegistryImpl innerPrefetchRegistry = innerView.mPrefetchRegistry; innerPrefetchRegistry.collectPrefetchPositionsFromView(innerView, true); if (innerPrefetchRegistry.mCount != 0) { @@ -208,7 +328,7 @@ private void prefetchInnerRecyclerViewWithDeadline(@Nullable RecyclerView innerV for (int i = 0; i < innerPrefetchRegistry.mCount * 2; i += 2) { // Note that we ignore immediate flag for inner items because // we have lower confidence they're needed next frame. - int innerPosition = innerPrefetchRegistry.mPrefetchArray[i]; + final int innerPosition = innerPrefetchRegistry.mPrefetchArray[i]; prefetchPositionWithDeadline(innerView, innerPosition, deadlineNs); } } finally { @@ -231,7 +351,7 @@ private void flushTaskWithDeadline(Task task, long deadlineNs) { private void flushTasksWithDeadline(long deadlineNs) { for (int i = 0; i < mTasks.size(); i++) { - Task task = mTasks.get(i); + final Task task = mTasks.get(i); if (task.view == null) { break; // done with populated tasks } @@ -257,7 +377,7 @@ public void run() { // Query most recent vsync so we can predict next one. Note that drawing time not yet // valid in animation/input callbacks, so query it here to be safe. - int size = mRecyclerViews.size(); + final int size = mRecyclerViews.size(); long latestFrameVsyncMs = 0; for (int i = 0; i < size; i++) { RecyclerView view = mRecyclerViews.get(i); @@ -281,118 +401,4 @@ public void run() { Trace.endSection(); } } - - static class Task { - public boolean immediate; - public int viewVelocity; - public int distanceToItem; - public RecyclerView view; - public int position; - - public void clear() { - immediate = false; - viewVelocity = 0; - distanceToItem = 0; - view = null; - position = 0; - } - } - - /** - * Prefetch information associated with a specific RecyclerView. - */ - @SuppressLint("VisibleForTests") - static class LayoutPrefetchRegistryImpl - implements RecyclerView.LayoutManager.LayoutPrefetchRegistry { - int mPrefetchDx; - int mPrefetchDy; - int[] mPrefetchArray; - - int mCount; - - void setPrefetchVector(int dx, int dy) { - mPrefetchDx = dx; - mPrefetchDy = dy; - } - - void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) { - mCount = 0; - if (mPrefetchArray != null) { - Arrays.fill(mPrefetchArray, -1); - } - - RecyclerView.LayoutManager layout = view.mLayout; - if (view.mAdapter != null - && layout != null - && layout.isItemPrefetchEnabled()) { - if (nested) { - // nested prefetch, only if no adapter updates pending. Note: we don't query - // view.hasPendingAdapterUpdates(), as first layout may not have occurred - if (!view.mAdapterHelper.hasPendingUpdates()) { - layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this); - } - } else { - // momentum based prefetch, only if we trust current child/adapter state - if (!view.hasPendingAdapterUpdates()) { - layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy, - view.mState, this); - } - } - - if (mCount > layout.mPrefetchMaxCountObserved) { - layout.mPrefetchMaxCountObserved = mCount; - layout.mPrefetchMaxObservedInInitialPrefetch = nested; - view.mRecycler.updateViewCacheSize(); - } - } - } - - @Override - public void addPosition(int layoutPosition, int pixelDistance) { - if (layoutPosition < 0) { - throw new IllegalArgumentException("Layout positions must be non-negative"); - } - - if (pixelDistance < 0) { - throw new IllegalArgumentException("Pixel distance must be non-negative"); - } - - // allocate or expand array as needed, doubling when needed - int storagePosition = mCount * 2; - if (mPrefetchArray == null) { - mPrefetchArray = new int[4]; - Arrays.fill(mPrefetchArray, -1); - } else if (storagePosition >= mPrefetchArray.length) { - int[] oldArray = mPrefetchArray; - mPrefetchArray = new int[storagePosition * 2]; - System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length); - } - - // add position - mPrefetchArray[storagePosition] = layoutPosition; - mPrefetchArray[storagePosition + 1] = pixelDistance; - - mCount++; - } - - boolean lastPrefetchIncludedPosition(int position) { - if (mPrefetchArray != null) { - int count = mCount * 2; - for (int i = 0; i < count; i += 2) { - if (mPrefetchArray[i] == position) return true; - } - } - return false; - } - - /** - * Called when prefetch indices are no longer valid for cache prioritization. - */ - void clearPrefetchPositions() { - if (mPrefetchArray != null) { - Arrays.fill(mPrefetchArray, -1); - } - mCount = 0; - } - } } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java b/viewpager2/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java index 3840dab3f..1534330c0 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java @@ -17,6 +17,7 @@ import android.content.Context; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; import android.util.SparseIntArray; @@ -25,6 +26,7 @@ import android.widget.GridView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import java.util.Arrays; @@ -37,39 +39,41 @@ */ public class GridLayoutManager extends LinearLayoutManager { - public static final int DEFAULT_SPAN_COUNT = -1; private static final boolean DEBUG = false; private static final String TAG = "GridLayoutManager"; - final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray(); - final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray(); - // re-used variable to acquire decor insets from RecyclerView - final Rect mDecorInsets = new Rect(); + public static final int DEFAULT_SPAN_COUNT = -1; + /** * Span size have been changed but we've not done a new layout calculation. */ - boolean mPendingSpanCountChange; + boolean mPendingSpanCountChange = false; int mSpanCount = DEFAULT_SPAN_COUNT; /** * Right borders for each span. *

For i-th item start is {@link #mCachedBorders}[i-1] + 1 * and end is {@link #mCachedBorders}[i]. */ - int[] mCachedBorders; + int [] mCachedBorders; /** * Temporary array to keep views in layoutChunk method */ View[] mSet; + final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray(); + final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray(); SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup(); + // re-used variable to acquire decor insets from RecyclerView + final Rect mDecorInsets = new Rect(); + private boolean mUsingSpansToEstimateScrollBarDimensions; /** * Constructor used when layout manager is set in XML by RecyclerView attribute * "layoutManager". If spanCount is not specified in the XML, it defaults to a * single column. - *

- * {@link androidx.recyclerview.R.attr#spanCount} + * + * {@link androidx.viewpager2.R.attr#spanCount} */ - public GridLayoutManager(@NonNull Context context, AttributeSet attrs, int defStyleAttr, + public GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); @@ -79,57 +83,27 @@ public GridLayoutManager(@NonNull Context context, AttributeSet attrs, int defSt /** * Creates a vertical GridLayoutManager * - * @param context Current context, will be used to access resources. + * @param context Current context, will be used to access resources. * @param spanCount The number of columns in the grid */ - public GridLayoutManager(@NonNull Context context, int spanCount) { + public GridLayoutManager(Context context, int spanCount) { super(context); setSpanCount(spanCount); } /** - * @param context Current context, will be used to access resources. - * @param spanCount The number of columns or rows in the grid - * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link + * @param context Current context, will be used to access resources. + * @param spanCount The number of columns or rows in the grid + * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link * #VERTICAL}. * @param reverseLayout When set to true, layouts from end to start. */ - public GridLayoutManager(@NonNull Context context, int spanCount, - @RecyclerView.Orientation int orientation, boolean reverseLayout) { + public GridLayoutManager(Context context, int spanCount, + @RecyclerView.Orientation int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); setSpanCount(spanCount); } - /** - * @param cachedBorders The out array - * @param spanCount number of spans - * @param totalSpace total available space after padding is removed - * @return The updated array. Might be the same instance as the provided array if its size - * has not changed. - */ - static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) { - if (cachedBorders == null || cachedBorders.length != spanCount + 1 - || cachedBorders[cachedBorders.length - 1] != totalSpace) { - cachedBorders = new int[spanCount + 1]; - } - cachedBorders[0] = 0; - int sizePerSpan = totalSpace / spanCount; - int sizePerSpanRemainder = totalSpace % spanCount; - int consumedPixels = 0; - int additionalSize = 0; - for (int i = 1; i <= spanCount; i++) { - int itemSize = sizePerSpan; - additionalSize += sizePerSpanRemainder; - if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) { - itemSize += 1; - additionalSize -= spanCount; - } - consumedPixels += itemSize; - cachedBorders[i] = consumedPixels; - } - return cachedBorders; - } - /** * stackFromEnd is not supported by GridLayoutManager. Consider using * {@link #setReverseLayout(boolean)}. @@ -145,10 +119,10 @@ public void setStackFromEnd(boolean stackFromEnd) { } @Override - public int getRowCountForAccessibility(@NonNull RecyclerView.Recycler recycler, - @NonNull RecyclerView.State state) { + public int getRowCountForAccessibility(RecyclerView.Recycler recycler, + RecyclerView.State state) { if (mOrientation == HORIZONTAL) { - return mSpanCount; + return Math.min(mSpanCount, getItemCount()); } if (state.getItemCount() < 1) { return 0; @@ -159,10 +133,10 @@ public int getRowCountForAccessibility(@NonNull RecyclerView.Recycler recycler, } @Override - public int getColumnCountForAccessibility(@NonNull RecyclerView.Recycler recycler, - @NonNull RecyclerView.State state) { + public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, + RecyclerView.State state) { if (mOrientation == VERTICAL) { - return mSpanCount; + return Math.min(mSpanCount, getItemCount()); } if (state.getItemCount() < 1) { return 0; @@ -173,11 +147,11 @@ public int getColumnCountForAccessibility(@NonNull RecyclerView.Recycler recycle } @Override - public void onInitializeAccessibilityNodeInfoForItem(@NonNull RecyclerView.Recycler recycler, - @NonNull RecyclerView.State state, View host, @NonNull AccessibilityNodeInfoCompat info) { + public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, + RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { ViewGroup.LayoutParams lp = host.getLayoutParams(); if (!(lp instanceof LayoutParams)) { - onInitializeAccessibilityNodeInfoForItem(host, info); + super.onInitializeAccessibilityNodeInfoForItem(host, info); return; } LayoutParams glp = (LayoutParams) lp; @@ -188,14 +162,14 @@ public void onInitializeAccessibilityNodeInfoForItem(@NonNull RecyclerView.Recyc spanGroupIndex, 1, false, false)); } else { // VERTICAL info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( - spanGroupIndex, 1, + spanGroupIndex , 1, glp.getSpanIndex(), glp.getSpanSize(), false, false)); } } @Override public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler, - @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) { + @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(recycler, state, info); // Set the class name so this is treated as a grid. A11y services should identify grids // and list via CollectionInfos, but an almost empty grid may be incorrectly identified @@ -204,7 +178,58 @@ public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler rec } @Override - public void onLayoutChildren(@NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state) { + boolean performAccessibilityAction(int action, @Nullable Bundle args) { + if (action == android.R.id.accessibilityActionScrollToPosition) { + final int noRow = -1; + final int noColumn = -1; + if (args != null) { + int rowArg = args.getInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_ROW_INT, + noRow); + int columnArg = args.getInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_COLUMN_INT, + noColumn); + + if (rowArg == noRow || columnArg == noColumn) { + return false; + } + + int itemCount = mRecyclerView.mAdapter.getItemCount(); + + int position = -1; + for (int i = 0; i < itemCount; i++) { + // Corresponds to a column value if the orientation is VERTICAL and a row value + // if the orientation is HORIZONTAL + int spanIndex = getSpanIndex(mRecyclerView.mRecycler, mRecyclerView.mState, i); + + // Corresponds to a row value if the orientation is VERTICAL and a column value + // if the orientation is HORIZONTAL + int spanGroupIndex = getSpanGroupIndex(mRecyclerView.mRecycler, + mRecyclerView.mState, i); + + if (mOrientation == VERTICAL) { + if (spanIndex == columnArg && spanGroupIndex == rowArg) { + position = i; + break; + } + } else { // horizontal + if (spanIndex == rowArg && spanGroupIndex == columnArg) { + position = i; + break; + } + } + } + + if (position > -1) { + scrollToPositionWithOffset(position, 0); + return true; + } + return false; + } + } + return super.performAccessibilityAction(action, args); + } + + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { if (state.isPreLayout()) { cachePreLayoutSpanMapping(); } @@ -216,7 +241,7 @@ public void onLayoutChildren(@NonNull RecyclerView.Recycler recycler, @NonNull R } @Override - public void onLayoutCompleted(@NonNull RecyclerView.State state) { + public void onLayoutCompleted(RecyclerView.State state) { super.onLayoutCompleted(state); mPendingSpanCountChange = false; } @@ -227,47 +252,46 @@ private void clearPreLayoutSpanMappingCache() { } private void cachePreLayoutSpanMapping() { - int childCount = getChildCount(); + final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { - LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); - int viewPosition = lp.getViewLayoutPosition(); + final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); + final int viewPosition = lp.getViewLayoutPosition(); mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize()); mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex()); } } @Override - public void onItemsAdded(@NonNull RecyclerView recyclerView, int positionStart, int itemCount) { + public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { mSpanSizeLookup.invalidateSpanIndexCache(); mSpanSizeLookup.invalidateSpanGroupIndexCache(); } @Override - public void onItemsChanged(@NonNull RecyclerView recyclerView) { + public void onItemsChanged(RecyclerView recyclerView) { mSpanSizeLookup.invalidateSpanIndexCache(); mSpanSizeLookup.invalidateSpanGroupIndexCache(); } @Override - public void onItemsRemoved(@NonNull RecyclerView recyclerView, int positionStart, int itemCount) { + public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { mSpanSizeLookup.invalidateSpanIndexCache(); mSpanSizeLookup.invalidateSpanGroupIndexCache(); } @Override - public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart, int itemCount, - Object payload) { + public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, + Object payload) { mSpanSizeLookup.invalidateSpanIndexCache(); mSpanSizeLookup.invalidateSpanGroupIndexCache(); } @Override - public void onItemsMoved(@NonNull RecyclerView recyclerView, int from, int to, int itemCount) { + public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { mSpanSizeLookup.invalidateSpanIndexCache(); mSpanSizeLookup.invalidateSpanGroupIndexCache(); } - @NonNull @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { if (mOrientation == HORIZONTAL) { @@ -298,15 +322,6 @@ public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { return lp instanceof LayoutParams; } - /** - * Returns the current {@link SpanSizeLookup} used by the GridLayoutManager. - * - * @return The current {@link SpanSizeLookup} used by the GridLayoutManager. - */ - public SpanSizeLookup getSpanSizeLookup() { - return mSpanSizeLookup; - } - /** * Sets the source to get the number of spans occupied by each item in the adapter. * @@ -317,6 +332,15 @@ public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) { mSpanSizeLookup = spanSizeLookup; } + /** + * Returns the current {@link SpanSizeLookup} used by the GridLayoutManager. + * + * @return The current {@link SpanSizeLookup} used by the GridLayoutManager. + */ + public SpanSizeLookup getSpanSizeLookup() { + return mSpanSizeLookup; + } + private void updateMeasurements() { int totalSpace; if (getOrientation() == VERTICAL) { @@ -332,16 +356,16 @@ public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { if (mCachedBorders == null) { super.setMeasuredDimension(childrenBounds, wSpec, hSpec); } - int width, height; - int horizontalPadding = getPaddingLeft() + getPaddingRight(); - int verticalPadding = getPaddingTop() + getPaddingBottom(); + final int width, height; + final int horizontalPadding = getPaddingLeft() + getPaddingRight(); + final int verticalPadding = getPaddingTop() + getPaddingBottom(); if (mOrientation == VERTICAL) { - int usedHeight = childrenBounds.height() + verticalPadding; + final int usedHeight = childrenBounds.height() + verticalPadding; height = chooseSize(hSpec, usedHeight, getMinimumHeight()); width = chooseSize(wSpec, mCachedBorders[mCachedBorders.length - 1] + horizontalPadding, getMinimumWidth()); } else { - int usedWidth = childrenBounds.width() + horizontalPadding; + final int usedWidth = childrenBounds.width() + horizontalPadding; width = chooseSize(wSpec, usedWidth, getMinimumWidth()); height = chooseSize(hSpec, mCachedBorders[mCachedBorders.length - 1] + verticalPadding, getMinimumHeight()); @@ -356,6 +380,36 @@ private void calculateItemBorders(int totalSpace) { mCachedBorders = calculateItemBorders(mCachedBorders, mSpanCount, totalSpace); } + /** + * @param cachedBorders The out array + * @param spanCount number of spans + * @param totalSpace total available space after padding is removed + * @return The updated array. Might be the same instance as the provided array if its size + * has not changed. + */ + static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) { + if (cachedBorders == null || cachedBorders.length != spanCount + 1 + || cachedBorders[cachedBorders.length - 1] != totalSpace) { + cachedBorders = new int[spanCount + 1]; + } + cachedBorders[0] = 0; + int sizePerSpan = totalSpace / spanCount; + int sizePerSpanRemainder = totalSpace % spanCount; + int consumedPixels = 0; + int additionalSize = 0; + for (int i = 1; i <= spanCount; i++) { + int itemSize = sizePerSpan; + additionalSize += sizePerSpanRemainder; + if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) { + itemSize += 1; + additionalSize -= spanCount; + } + consumedPixels += itemSize; + cachedBorders[i] = consumedPixels; + } + return cachedBorders; + } + int getSpaceForSpanRange(int startSpan, int spanSize) { if (mOrientation == VERTICAL && isLayoutRTL()) { return mCachedBorders[mSpanCount - startSpan] @@ -383,24 +437,24 @@ private void ensureViewSet() { } @Override - public int scrollHorizontallyBy(int dx, @NonNull RecyclerView.Recycler recycler, - @NonNull RecyclerView.State state) { + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, + RecyclerView.State state) { updateMeasurements(); ensureViewSet(); return super.scrollHorizontallyBy(dx, recycler, state); } @Override - public int scrollVerticallyBy(int dy, @NonNull RecyclerView.Recycler recycler, - @NonNull RecyclerView.State state) { + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, + RecyclerView.State state) { updateMeasurements(); ensureViewSet(); return super.scrollVerticallyBy(dy, recycler, state); } private void ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler, - RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection) { - boolean layingOutInPrimaryDirection = + RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection) { + final boolean layingOutInPrimaryDirection = itemDirection == LayoutState.ITEM_DIRECTION_TAIL; int span = getSpanIndex(recycler, state, anchorInfo.mPosition); if (layingOutInPrimaryDirection) { @@ -411,7 +465,7 @@ private void ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler, } } else { // choose the max span we can get. hopefully last one - int indexLimit = state.getItemCount() - 1; + final int indexLimit = state.getItemCount() - 1; int pos = anchorInfo.mPosition; int bestSpan = span; while (pos < indexLimit) { @@ -429,7 +483,7 @@ private void ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler, @Override View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, - boolean layoutFromEnd, boolean traverseChildrenInReverseOrder) { + boolean layoutFromEnd, boolean traverseChildrenInReverseOrder) { int start = 0; int end = getChildCount(); @@ -446,14 +500,14 @@ View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state View invalidMatch = null; View outOfBoundsMatch = null; - int boundsStart = mOrientationHelper.getStartAfterPadding(); - int boundsEnd = mOrientationHelper.getEndAfterPadding(); + final int boundsStart = mOrientationHelper.getStartAfterPadding(); + final int boundsEnd = mOrientationHelper.getEndAfterPadding(); for (int i = start; i != end; i += diff) { - View view = getChildAt(i); - int position = getPosition(view); + final View view = getChildAt(i); + final int position = getPosition(view); if (position >= 0 && position < itemCount) { - int span = getSpanIndex(recycler, state, position); + final int span = getSpanIndex(recycler, state, position); if (span != 0) { continue; } @@ -475,11 +529,11 @@ View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state } private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state, - int viewPosition) { + int viewPosition) { if (!state.isPreLayout()) { return mSpanSizeLookup.getCachedSpanGroupIndex(viewPosition, mSpanCount); } - int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition); + final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition); if (adapterPosition == -1) { if (DEBUG) { throw new RuntimeException("Cannot find span group index for position " @@ -495,11 +549,11 @@ private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State stat if (!state.isPreLayout()) { return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount); } - int cached = mPreLayoutSpanIndexCache.get(pos, -1); + final int cached = mPreLayoutSpanIndexCache.get(pos, -1); if (cached != -1) { return cached; } - int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos); + final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos); if (adapterPosition == -1) { if (DEBUG) { throw new RuntimeException("Cannot find span index for pre layout position. It is" @@ -516,11 +570,11 @@ private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state if (!state.isPreLayout()) { return mSpanSizeLookup.getSpanSize(pos); } - int cached = mPreLayoutSpanSizeCache.get(pos, -1); + final int cached = mPreLayoutSpanSizeCache.get(pos, -1); if (cached != -1) { return cached; } - int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos); + final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos); if (adapterPosition == -1) { if (DEBUG) { throw new RuntimeException("Cannot find span size for pre layout position. It is" @@ -535,13 +589,13 @@ private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state @Override void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState, - LayoutPrefetchRegistry layoutPrefetchRegistry) { + LayoutPrefetchRegistry layoutPrefetchRegistry) { int remainingSpan = mSpanCount; int count = 0; while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) { - int pos = layoutState.mCurrentPosition; + final int pos = layoutState.mCurrentPosition; layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset)); - int spanSize = mSpanSizeLookup.getSpanSize(pos); + final int spanSize = mSpanSizeLookup.getSpanSize(pos); remainingSpan -= spanSize; layoutState.mCurrentPosition += layoutState.mItemDirection; count++; @@ -550,17 +604,17 @@ void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutStat @Override void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, - LayoutState layoutState, LayoutChunkResult result) { - int otherDirSpecMode = mOrientationHelper.getModeInOther(); - boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY; - int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0; + LayoutState layoutState, LayoutChunkResult result) { + final int otherDirSpecMode = mOrientationHelper.getModeInOther(); + final boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY; + final int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0; // if grid layout's dimensions are not specified, let the new row change the measurements // This is not perfect since we not covering all rows but still solves an important case // where they may have a header row which should be laid out according to children. if (flexibleInOtherDir) { updateMeasurements(); // reset measurements } - boolean layingOutInPrimaryDirection = + final boolean layingOutInPrimaryDirection = layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL; int count = 0; int remainingSpan = mSpanCount; @@ -571,7 +625,7 @@ void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, } while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) { int pos = layoutState.mCurrentPosition; - int spanSize = getSpanSize(recycler, state, pos); + final int spanSize = getSpanSize(recycler, state, pos); if (spanSize > mSpanCount) { throw new IllegalArgumentException("Item at position " + pos + " requires " + spanSize + " spans but GridLayoutManager has only " + mSpanCount @@ -617,12 +671,12 @@ void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, calculateItemDecorationsForChild(view, mDecorInsets); measureChild(view, otherDirSpecMode, false); - int size = mOrientationHelper.getDecoratedMeasurement(view); + final int size = mOrientationHelper.getDecoratedMeasurement(view); if (size > maxSize) { maxSize = size; } - LayoutParams lp = (LayoutParams) view.getLayoutParams(); - float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view) + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view) / lp.mSpanSize; if (otherSize > maxSizeInOther) { maxSizeInOther = otherSize; @@ -636,7 +690,7 @@ void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, for (int i = 0; i < count; i++) { View view = mSet[i]; measureChild(view, View.MeasureSpec.EXACTLY, true); - int size = mOrientationHelper.getDecoratedMeasurement(view); + final int size = mOrientationHelper.getDecoratedMeasurement(view); if (size > maxSize) { maxSize = size; } @@ -646,17 +700,17 @@ void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, // Views that did not measure the maxSize has to be re-measured // We will stop doing this once we introduce Gravity in the GLM layout params for (int i = 0; i < count; i++) { - View view = mSet[i]; + final View view = mSet[i]; if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) { - LayoutParams lp = (LayoutParams) view.getLayoutParams(); - Rect decorInsets = lp.mDecorInsets; - int verticalInsets = decorInsets.top + decorInsets.bottom + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + final Rect decorInsets = lp.mDecorInsets; + final int verticalInsets = decorInsets.top + decorInsets.bottom + lp.topMargin + lp.bottomMargin; - int horizontalInsets = decorInsets.left + decorInsets.right + final int horizontalInsets = decorInsets.left + decorInsets.right + lp.leftMargin + lp.rightMargin; - int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize); - int wSpec; - int hSpec; + final int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize); + final int wSpec; + final int hSpec; if (mOrientation == VERTICAL) { wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY, horizontalInsets, lp.width, false); @@ -729,21 +783,21 @@ void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, * Measures a child with currently known information. This is not necessarily the child's final * measurement. (see fillChunk for details). * - * @param view The child view to be measured + * @param view The child view to be measured * @param otherDirParentSpecMode The RV measure spec that should be used in the secondary * orientation - * @param alreadyMeasured True if we've already measured this view once + * @param alreadyMeasured True if we've already measured this view once */ private void measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured) { - LayoutParams lp = (LayoutParams) view.getLayoutParams(); - Rect decorInsets = lp.mDecorInsets; - int verticalInsets = decorInsets.top + decorInsets.bottom + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + final Rect decorInsets = lp.mDecorInsets; + final int verticalInsets = decorInsets.top + decorInsets.bottom + lp.topMargin + lp.bottomMargin; - int horizontalInsets = decorInsets.left + decorInsets.right + final int horizontalInsets = decorInsets.left + decorInsets.right + lp.leftMargin + lp.rightMargin; - int availableSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize); - int wSpec; - int hSpec; + final int availableSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize); + final int wSpec; + final int hSpec; if (mOrientation == VERTICAL) { wSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode, horizontalInsets, lp.width, false); @@ -765,19 +819,19 @@ private void measureChild(View view, int otherDirParentSpecMode, boolean already * Here we try to assign a best guess width or height and re-do the layout to update other * views that wanted to MATCH_PARENT in the non-scroll orientation. * - * @param maxSizeInOther The maximum size per span ratio from the measurement of the children. + * @param maxSizeInOther The maximum size per span ratio from the measurement of the children. * @param currentOtherDirSize The size before this layout chunk. There is no reason to go below. */ private void guessMeasurement(float maxSizeInOther, int currentOtherDirSize) { - int contentSize = Math.round(maxSizeInOther * mSpanCount); + final int contentSize = Math.round(maxSizeInOther * mSpanCount); // always re-calculate because borders were stretched during the fill calculateItemBorders(Math.max(contentSize, currentOtherDirSize)); } private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec, - boolean alreadyMeasured) { + boolean alreadyMeasured) { RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); - boolean measure; + final boolean measure; if (alreadyMeasured) { measure = shouldReMeasureChild(child, widthSpec, heightSpec, lp); } else { @@ -789,7 +843,7 @@ private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int } private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, - boolean layingOutInPrimaryDirection) { + boolean layingOutInPrimaryDirection) { // spans are always assigned from 0 to N no matter if it is RTL or not. // RTL is used only when positioning the view. int span, start, end, diff; @@ -846,338 +900,68 @@ public void setSpanCount(int spanCount) { requestLayout(); } - @Override - public View onFocusSearchFailed(@NonNull View focused, int direction, - @NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state) { - View prevFocusedChild = findContainingItemView(focused); - if (prevFocusedChild == null) { - return null; - } - LayoutParams lp = (LayoutParams) prevFocusedChild.getLayoutParams(); - int prevSpanStart = lp.mSpanIndex; - int prevSpanEnd = lp.mSpanIndex + lp.mSpanSize; - View view = super.onFocusSearchFailed(focused, direction, recycler, state); - if (view == null) { - return null; - } - // LinearLayoutManager finds the last child. What we want is the child which has the same - // spanIndex. - int layoutDir = convertFocusDirectionToLayoutDirection(direction); - boolean ascend = (layoutDir == LayoutState.LAYOUT_END) != mShouldReverseLayout; - int start, inc, limit; - if (ascend) { - start = getChildCount() - 1; - inc = -1; - limit = -1; - } else { - start = 0; - inc = 1; - limit = getChildCount(); - } - boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL(); - - // The focusable candidate to be picked if no perfect focusable candidate is found. - // The best focusable candidate is the one with the highest amount of span overlap with - // the currently focused view. - View focusableWeakCandidate = null; // somewhat matches but not strong - int focusableWeakCandidateSpanIndex = -1; - int focusableWeakCandidateOverlap = 0; // how many spans overlap + /** + * A helper class to provide the number of spans each item occupies. + *

+ * Default implementation sets each item to occupy exactly 1 span. + * + * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup) + */ + public abstract static class SpanSizeLookup { - // The unfocusable candidate to become visible on the screen next, if no perfect or - // weak focusable candidates are found to receive focus next. - // We are only interested in partially visible unfocusable views. These are views that are - // not fully visible, that is either partially overlapping, or out-of-bounds and right below - // or above RV's padded bounded area. The best unfocusable candidate is the one with the - // highest amount of span overlap with the currently focused view. - View unfocusableWeakCandidate = null; // somewhat matches but not strong - int unfocusableWeakCandidateSpanIndex = -1; - int unfocusableWeakCandidateOverlap = 0; // how many spans overlap + final SparseIntArray mSpanIndexCache = new SparseIntArray(); + final SparseIntArray mSpanGroupIndexCache = new SparseIntArray(); - // The span group index of the start child. This indicates the span group index of the - // next focusable item to receive focus, if a focusable item within the same span group - // exists. Any focusable item beyond this group index are not relevant since they - // were already stored in the layout before onFocusSearchFailed call and were not picked - // by the focusSearch algorithm. - int focusableSpanGroupIndex = getSpanGroupIndex(recycler, state, start); - for (int i = start; i != limit; i += inc) { - int spanGroupIndex = getSpanGroupIndex(recycler, state, i); - View candidate = getChildAt(i); - if (candidate == prevFocusedChild) { - break; - } + private boolean mCacheSpanIndices = false; + private boolean mCacheSpanGroupIndices = false; - if (candidate.hasFocusable() && spanGroupIndex != focusableSpanGroupIndex) { - // We are past the allowable span group index for the next focusable item. - // The search only continues if no focusable weak candidates have been found up - // until this point, in order to find the best unfocusable candidate to become - // visible on the screen next. - if (focusableWeakCandidate != null) { - break; - } - continue; - } + /** + * Returns the number of span occupied by the item at position. + * + * @param position The adapter position of the item + * @return The number of spans occupied by the item at the provided position + */ + public abstract int getSpanSize(int position); - LayoutParams candidateLp = (LayoutParams) candidate.getLayoutParams(); - int candidateStart = candidateLp.mSpanIndex; - int candidateEnd = candidateLp.mSpanIndex + candidateLp.mSpanSize; - if (candidate.hasFocusable() && candidateStart == prevSpanStart - && candidateEnd == prevSpanEnd) { - return candidate; // perfect match - } - boolean assignAsWeek = false; - if ((candidate.hasFocusable() && focusableWeakCandidate == null) - || (!candidate.hasFocusable() && unfocusableWeakCandidate == null)) { - assignAsWeek = true; - } else { - int maxStart = Math.max(candidateStart, prevSpanStart); - int minEnd = Math.min(candidateEnd, prevSpanEnd); - int overlap = minEnd - maxStart; - if (candidate.hasFocusable()) { - if (overlap > focusableWeakCandidateOverlap) { - assignAsWeek = true; - } else if (overlap == focusableWeakCandidateOverlap - && preferLastSpan == (candidateStart - > focusableWeakCandidateSpanIndex)) { - assignAsWeek = true; - } - } else if (focusableWeakCandidate == null - && isViewPartiallyVisible(candidate, false, true)) { - if (overlap > unfocusableWeakCandidateOverlap) { - assignAsWeek = true; - } else if (overlap == unfocusableWeakCandidateOverlap - && preferLastSpan == (candidateStart - > unfocusableWeakCandidateSpanIndex)) { - assignAsWeek = true; - } - } + /** + * Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or + * not. By default these values are not cached. If you are not overriding + * {@link #getSpanIndex(int, int)} with something highly performant, you should set this + * to true for better performance. + * + * @param cacheSpanIndices Whether results of getSpanIndex should be cached or not. + */ + public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) { + if (!cacheSpanIndices) { + mSpanGroupIndexCache.clear(); } + mCacheSpanIndices = cacheSpanIndices; + } - if (assignAsWeek) { - int focusableWeakCandidateOverlap1 = Math.min(candidateEnd, prevSpanEnd) - - Math.max(candidateStart, prevSpanStart); - if (candidate.hasFocusable()) { - focusableWeakCandidate = candidate; - focusableWeakCandidateSpanIndex = candidateLp.mSpanIndex; - focusableWeakCandidateOverlap = focusableWeakCandidateOverlap1; - } else { - unfocusableWeakCandidate = candidate; - unfocusableWeakCandidateSpanIndex = candidateLp.mSpanIndex; - unfocusableWeakCandidateOverlap = focusableWeakCandidateOverlap1; - } + /** + * Sets whether the results of {@link #getSpanGroupIndex(int, int)} method should be cached + * or not. By default these values are not cached. If you are not overriding + * {@link #getSpanGroupIndex(int, int)} with something highly performant, and you are using + * spans to calculate scrollbar offset and range, you should set this to true for better + * performance. + * + * @param cacheSpanGroupIndices Whether results of getGroupSpanIndex should be cached or + * not. + */ + public void setSpanGroupIndexCacheEnabled(boolean cacheSpanGroupIndices) { + if (!cacheSpanGroupIndices) { + mSpanGroupIndexCache.clear(); } + mCacheSpanGroupIndices = cacheSpanGroupIndices; } - return (focusableWeakCandidate != null) ? focusableWeakCandidate : unfocusableWeakCandidate; - } - @Override - public boolean supportsPredictiveItemAnimations() { - return mPendingSavedState == null && !mPendingSpanCountChange; - } - - @Override - public int computeHorizontalScrollRange(@NonNull RecyclerView.State state) { - if (mUsingSpansToEstimateScrollBarDimensions) { - return computeScrollRangeWithSpanInfo(state); - } else { - return super.computeHorizontalScrollRange(state); - } - } - - @Override - public int computeVerticalScrollRange(@NonNull RecyclerView.State state) { - if (mUsingSpansToEstimateScrollBarDimensions) { - return computeScrollRangeWithSpanInfo(state); - } else { - return super.computeVerticalScrollRange(state); - } - } - - @Override - public int computeHorizontalScrollOffset(@NonNull RecyclerView.State state) { - if (mUsingSpansToEstimateScrollBarDimensions) { - return computeScrollOffsetWithSpanInfo(state); - } else { - return super.computeHorizontalScrollOffset(state); - } - } - - @Override - public int computeVerticalScrollOffset(@NonNull RecyclerView.State state) { - if (mUsingSpansToEstimateScrollBarDimensions) { - return computeScrollOffsetWithSpanInfo(state); - } else { - return super.computeVerticalScrollOffset(state); - } - } - - /** - * Returns true if the scroll offset and scroll range calculations take account of span - * information. See {@link #setUsingSpansToEstimateScrollbarDimensions(boolean)} for more - * information on this topic. Defaults to {@code false}. - * - * @return true if the scroll offset and scroll range calculations take account of span - * information. - */ - public boolean isUsingSpansToEstimateScrollbarDimensions() { - return mUsingSpansToEstimateScrollBarDimensions; - } - - /** - * When this flag is set, the scroll offset and scroll range calculations will take account - * of span information. - * - *

This is will increase the accuracy of the scroll bar's size and offset but will require - * more calls to {@link SpanSizeLookup#getSpanGroupIndex(int, int)}". - * - *

This additional accuracy may or may not be needed, depending on the characteristics of - * your layout. You will likely benefit from this accuracy when: - * - *

    - *
  • The variation in item span sizes is large. - *
  • The size of your data set is small (if your data set is large, the scrollbar will - * likely be very small anyway, and thus the increased accuracy has less impact). - *
  • Calls to {@link SpanSizeLookup#getSpanGroupIndex(int, int)} are fast. - *
- * - *

If you decide to enable this feature, you should be sure that calls to - * {@link SpanSizeLookup#getSpanGroupIndex(int, int)} are fast, that set span group index - * caching is set to true via a call to - * {@link SpanSizeLookup#setSpanGroupIndexCacheEnabled(boolean), - * and span index caching is also enabled via a call to - * {@link SpanSizeLookup#setSpanIndexCacheEnabled(boolean)}}. - */ - public void setUsingSpansToEstimateScrollbarDimensions( - boolean useSpansToEstimateScrollBarDimensions) { - mUsingSpansToEstimateScrollBarDimensions = useSpansToEstimateScrollBarDimensions; - } - - private int computeScrollRangeWithSpanInfo(RecyclerView.State state) { - if (getChildCount() == 0 || state.getItemCount() == 0) { - return 0; - } - ensureLayoutState(); - - View startChild = findFirstVisibleChildClosestToStart(!isSmoothScrollbarEnabled(), true); - View endChild = findFirstVisibleChildClosestToEnd(!isSmoothScrollbarEnabled(), true); - - if (startChild == null || endChild == null) { - return 0; - } - if (!isSmoothScrollbarEnabled()) { - return mSpanSizeLookup.getCachedSpanGroupIndex( - state.getItemCount() - 1, mSpanCount) + 1; - } - - // smooth scrollbar enabled. try to estimate better. - int laidOutArea = mOrientationHelper.getDecoratedEnd(endChild) - - mOrientationHelper.getDecoratedStart(startChild); - - int firstVisibleSpan = - mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), mSpanCount); - int lastVisibleSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild), - mSpanCount); - int totalSpans = mSpanSizeLookup.getCachedSpanGroupIndex(state.getItemCount() - 1, - mSpanCount) + 1; - int laidOutSpans = lastVisibleSpan - firstVisibleSpan + 1; - - // estimate a size for full list. - return (int) (((float) laidOutArea / laidOutSpans) * totalSpans); - } - - private int computeScrollOffsetWithSpanInfo(RecyclerView.State state) { - if (getChildCount() == 0 || state.getItemCount() == 0) { - return 0; - } - ensureLayoutState(); - - boolean smoothScrollEnabled = isSmoothScrollbarEnabled(); - View startChild = findFirstVisibleChildClosestToStart(!smoothScrollEnabled, true); - View endChild = findFirstVisibleChildClosestToEnd(!smoothScrollEnabled, true); - if (startChild == null || endChild == null) { - return 0; - } - int startChildSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), - mSpanCount); - int endChildSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild), - mSpanCount); - - int minSpan = Math.min(startChildSpan, endChildSpan); - int maxSpan = Math.max(startChildSpan, endChildSpan); - int totalSpans = mSpanSizeLookup.getCachedSpanGroupIndex(state.getItemCount() - 1, - mSpanCount) + 1; - - int spansBefore = mShouldReverseLayout - ? Math.max(0, totalSpans - maxSpan - 1) - : Math.max(0, minSpan); - if (!smoothScrollEnabled) { - return spansBefore; - } - int laidOutArea = Math.abs(mOrientationHelper.getDecoratedEnd(endChild) - - mOrientationHelper.getDecoratedStart(startChild)); - - int firstVisibleSpan = - mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), mSpanCount); - int lastVisibleSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild), - mSpanCount); - int laidOutSpans = lastVisibleSpan - firstVisibleSpan + 1; - float avgSizePerSpan = (float) laidOutArea / laidOutSpans; - - return Math.round(spansBefore * avgSizePerSpan + (mOrientationHelper.getStartAfterPadding() - - mOrientationHelper.getDecoratedStart(startChild))); - } - - /** - * A helper class to provide the number of spans each item occupies. - *

- * Default implementation sets each item to occupy exactly 1 span. - * - * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup) - */ - public abstract static class SpanSizeLookup { - - final SparseIntArray mSpanIndexCache = new SparseIntArray(); - final SparseIntArray mSpanGroupIndexCache = new SparseIntArray(); - - private boolean mCacheSpanIndices; - private boolean mCacheSpanGroupIndices; - - static int findFirstKeyLessThan(SparseIntArray cache, int position) { - int lo = 0; - int hi = cache.size() - 1; - - while (lo <= hi) { - // Using unsigned shift here to divide by two because it is guaranteed to not - // overflow. - int mid = (lo + hi) >>> 1; - int midVal = cache.keyAt(mid); - if (midVal < position) { - lo = mid + 1; - } else { - hi = mid - 1; - } - } - int index = lo - 1; - if (index >= 0 && index < cache.size()) { - return cache.keyAt(index); - } - return -1; - } - - /** - * Returns the number of span occupied by the item at position. - * - * @param position The adapter position of the item - * @return The number of spans occupied by the item at the provided position - */ - public abstract int getSpanSize(int position); - - /** - * Clears the span index cache. GridLayoutManager automatically calls this method when - * adapter changes occur. - */ - public void invalidateSpanIndexCache() { - mSpanIndexCache.clear(); - } + /** + * Clears the span index cache. GridLayoutManager automatically calls this method when + * adapter changes occur. + */ + public void invalidateSpanIndexCache() { + mSpanIndexCache.clear(); + } /** * Clears the span group index cache. GridLayoutManager automatically calls this method @@ -1196,21 +980,6 @@ public boolean isSpanIndexCacheEnabled() { return mCacheSpanIndices; } - /** - * Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or - * not. By default these values are not cached. If you are not overriding - * {@link #getSpanIndex(int, int)} with something highly performant, you should set this - * to true for better performance. - * - * @param cacheSpanIndices Whether results of getSpanIndex should be cached or not. - */ - public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) { - if (!cacheSpanIndices) { - mSpanGroupIndexCache.clear(); - } - mCacheSpanIndices = cacheSpanIndices; - } - /** * Returns whether results of {@link #getSpanGroupIndex(int, int)} method are cached or not. * @@ -1220,32 +989,15 @@ public boolean isSpanGroupIndexCacheEnabled() { return mCacheSpanGroupIndices; } - /** - * Sets whether the results of {@link #getSpanGroupIndex(int, int)} method should be cached - * or not. By default these values are not cached. If you are not overriding - * {@link #getSpanGroupIndex(int, int)} with something highly performant, and you are using - * spans to calculate scrollbar offset and range, you should set this to true for better - * performance. - * - * @param cacheSpanGroupIndices Whether results of getGroupSpanIndex should be cached or - * not. - */ - public void setSpanGroupIndexCacheEnabled(boolean cacheSpanGroupIndices) { - if (!cacheSpanGroupIndices) { - mSpanGroupIndexCache.clear(); - } - mCacheSpanGroupIndices = cacheSpanGroupIndices; - } - int getCachedSpanIndex(int position, int spanCount) { if (!mCacheSpanIndices) { return getSpanIndex(position, spanCount); } - int existing = mSpanIndexCache.get(position, -1); + final int existing = mSpanIndexCache.get(position, -1); if (existing != -1) { return existing; } - int value = getSpanIndex(position, spanCount); + final int value = getSpanIndex(position, spanCount); mSpanIndexCache.put(position, value); return value; } @@ -1254,11 +1006,11 @@ int getCachedSpanGroupIndex(int position, int spanCount) { if (!mCacheSpanGroupIndices) { return getSpanGroupIndex(position, spanCount); } - int existing = mSpanGroupIndexCache.get(position, -1); + final int existing = mSpanGroupIndexCache.get(position, -1); if (existing != -1) { return existing; } - int value = getSpanGroupIndex(position, spanCount); + final int value = getSpanGroupIndex(position, spanCount); mSpanGroupIndexCache.put(position, value); return value; } @@ -1266,6 +1018,9 @@ int getCachedSpanGroupIndex(int position, int spanCount) { /** * Returns the final span index of the provided position. *

+ * If {@link #getOrientation()} is {@link #VERTICAL}, this is a column value. + * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is a row value. + *

* If you have a faster way to calculate span index for your items, you should override * this method. Otherwise, you should enable span index cache * ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is @@ -1317,14 +1072,39 @@ public int getSpanIndex(int position, int spanCount) { return 0; } + static int findFirstKeyLessThan(SparseIntArray cache, int position) { + int lo = 0; + int hi = cache.size() - 1; + + while (lo <= hi) { + // Using unsigned shift here to divide by two because it is guaranteed to not + // overflow. + final int mid = (lo + hi) >>> 1; + final int midVal = cache.keyAt(mid); + if (midVal < position) { + lo = mid + 1; + } else { + hi = mid - 1; + } + } + int index = lo - 1; + if (index >= 0 && index < cache.size()) { + return cache.keyAt(index); + } + return -1; + } + /** * Returns the index of the group this position belongs. *

+ * If {@link #getOrientation()} is {@link #VERTICAL}, this is a row value. + * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is a column value. + *

* For example, if grid has 3 columns and each item occupies 1 span, span group index * for item 1 will be 0, item 5 will be 1. * * @param adapterPosition The position in adapter - * @param spanCount The total number of spans in the grid + * @param spanCount The total number of spans in the grid * @return The index of the span group including the item at the given adapter position */ public int getSpanGroupIndex(int adapterPosition, int spanCount) { @@ -1364,6 +1144,286 @@ public int getSpanGroupIndex(int adapterPosition, int spanCount) { } } + @Override + public View onFocusSearchFailed(View focused, int direction, + RecyclerView.Recycler recycler, RecyclerView.State state) { + View prevFocusedChild = findContainingItemView(focused); + if (prevFocusedChild == null) { + return null; + } + LayoutParams lp = (LayoutParams) prevFocusedChild.getLayoutParams(); + final int prevSpanStart = lp.mSpanIndex; + final int prevSpanEnd = lp.mSpanIndex + lp.mSpanSize; + View view = super.onFocusSearchFailed(focused, direction, recycler, state); + if (view == null) { + return null; + } + // LinearLayoutManager finds the last child. What we want is the child which has the same + // spanIndex. + final int layoutDir = convertFocusDirectionToLayoutDirection(direction); + final boolean ascend = (layoutDir == LayoutState.LAYOUT_END) != mShouldReverseLayout; + final int start, inc, limit; + if (ascend) { + start = getChildCount() - 1; + inc = -1; + limit = -1; + } else { + start = 0; + inc = 1; + limit = getChildCount(); + } + final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL(); + + // The focusable candidate to be picked if no perfect focusable candidate is found. + // The best focusable candidate is the one with the highest amount of span overlap with + // the currently focused view. + View focusableWeakCandidate = null; // somewhat matches but not strong + int focusableWeakCandidateSpanIndex = -1; + int focusableWeakCandidateOverlap = 0; // how many spans overlap + + // The unfocusable candidate to become visible on the screen next, if no perfect or + // weak focusable candidates are found to receive focus next. + // We are only interested in partially visible unfocusable views. These are views that are + // not fully visible, that is either partially overlapping, or out-of-bounds and right below + // or above RV's padded bounded area. The best unfocusable candidate is the one with the + // highest amount of span overlap with the currently focused view. + View unfocusableWeakCandidate = null; // somewhat matches but not strong + int unfocusableWeakCandidateSpanIndex = -1; + int unfocusableWeakCandidateOverlap = 0; // how many spans overlap + + // The span group index of the start child. This indicates the span group index of the + // next focusable item to receive focus, if a focusable item within the same span group + // exists. Any focusable item beyond this group index are not relevant since they + // were already stored in the layout before onFocusSearchFailed call and were not picked + // by the focusSearch algorithm. + int focusableSpanGroupIndex = getSpanGroupIndex(recycler, state, start); + for (int i = start; i != limit; i += inc) { + int spanGroupIndex = getSpanGroupIndex(recycler, state, i); + View candidate = getChildAt(i); + if (candidate == prevFocusedChild) { + break; + } + + if (candidate.hasFocusable() && spanGroupIndex != focusableSpanGroupIndex) { + // We are past the allowable span group index for the next focusable item. + // The search only continues if no focusable weak candidates have been found up + // until this point, in order to find the best unfocusable candidate to become + // visible on the screen next. + if (focusableWeakCandidate != null) { + break; + } + continue; + } + + final LayoutParams candidateLp = (LayoutParams) candidate.getLayoutParams(); + final int candidateStart = candidateLp.mSpanIndex; + final int candidateEnd = candidateLp.mSpanIndex + candidateLp.mSpanSize; + if (candidate.hasFocusable() && candidateStart == prevSpanStart + && candidateEnd == prevSpanEnd) { + return candidate; // perfect match + } + boolean assignAsWeek = false; + if ((candidate.hasFocusable() && focusableWeakCandidate == null) + || (!candidate.hasFocusable() && unfocusableWeakCandidate == null)) { + assignAsWeek = true; + } else { + int maxStart = Math.max(candidateStart, prevSpanStart); + int minEnd = Math.min(candidateEnd, prevSpanEnd); + int overlap = minEnd - maxStart; + if (candidate.hasFocusable()) { + if (overlap > focusableWeakCandidateOverlap) { + assignAsWeek = true; + } else if (overlap == focusableWeakCandidateOverlap + && preferLastSpan == (candidateStart + > focusableWeakCandidateSpanIndex)) { + assignAsWeek = true; + } + } else if (focusableWeakCandidate == null + && isViewPartiallyVisible(candidate, false, true)) { + if (overlap > unfocusableWeakCandidateOverlap) { + assignAsWeek = true; + } else if (overlap == unfocusableWeakCandidateOverlap + && preferLastSpan == (candidateStart + > unfocusableWeakCandidateSpanIndex)) { + assignAsWeek = true; + } + } + } + + if (assignAsWeek) { + if (candidate.hasFocusable()) { + focusableWeakCandidate = candidate; + focusableWeakCandidateSpanIndex = candidateLp.mSpanIndex; + focusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) + - Math.max(candidateStart, prevSpanStart); + } else { + unfocusableWeakCandidate = candidate; + unfocusableWeakCandidateSpanIndex = candidateLp.mSpanIndex; + unfocusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) + - Math.max(candidateStart, prevSpanStart); + } + } + } + return (focusableWeakCandidate != null) ? focusableWeakCandidate : unfocusableWeakCandidate; + } + + @Override + public boolean supportsPredictiveItemAnimations() { + return mPendingSavedState == null && !mPendingSpanCountChange; + } + + @Override + public int computeHorizontalScrollRange(RecyclerView.State state) { + if (mUsingSpansToEstimateScrollBarDimensions) { + return computeScrollRangeWithSpanInfo(state); + } else { + return super.computeHorizontalScrollRange(state); + } + } + + @Override + public int computeVerticalScrollRange(RecyclerView.State state) { + if (mUsingSpansToEstimateScrollBarDimensions) { + return computeScrollRangeWithSpanInfo(state); + } else { + return super.computeVerticalScrollRange(state); + } + } + + @Override + public int computeHorizontalScrollOffset(RecyclerView.State state) { + if (mUsingSpansToEstimateScrollBarDimensions) { + return computeScrollOffsetWithSpanInfo(state); + } else { + return super.computeHorizontalScrollOffset(state); + } + } + + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + if (mUsingSpansToEstimateScrollBarDimensions) { + return computeScrollOffsetWithSpanInfo(state); + } else { + return super.computeVerticalScrollOffset(state); + } + } + + /** + * When this flag is set, the scroll offset and scroll range calculations will take account + * of span information. + * + *

This is will increase the accuracy of the scroll bar's size and offset but will require + * more calls to {@link SpanSizeLookup#getSpanGroupIndex(int, int)}". + * + *

This additional accuracy may or may not be needed, depending on the characteristics of + * your layout. You will likely benefit from this accuracy when: + * + *

    + *
  • The variation in item span sizes is large. + *
  • The size of your data set is small (if your data set is large, the scrollbar will + * likely be very small anyway, and thus the increased accuracy has less impact). + *
  • Calls to {@link SpanSizeLookup#getSpanGroupIndex(int, int)} are fast. + *
+ * + *

If you decide to enable this feature, you should be sure that calls to + * {@link SpanSizeLookup#getSpanGroupIndex(int, int)} are fast, that set span group index + * caching is set to true via a call to + * {@link SpanSizeLookup#setSpanGroupIndexCacheEnabled(boolean), + * and span index caching is also enabled via a call to + * {@link SpanSizeLookup#setSpanIndexCacheEnabled(boolean)}}. + */ + public void setUsingSpansToEstimateScrollbarDimensions( + boolean useSpansToEstimateScrollBarDimensions) { + mUsingSpansToEstimateScrollBarDimensions = useSpansToEstimateScrollBarDimensions; + } + + /** + * Returns true if the scroll offset and scroll range calculations take account of span + * information. See {@link #setUsingSpansToEstimateScrollbarDimensions(boolean)} for more + * information on this topic. Defaults to {@code false}. + * + * @return true if the scroll offset and scroll range calculations take account of span + * information. + */ + public boolean isUsingSpansToEstimateScrollbarDimensions() { + return mUsingSpansToEstimateScrollBarDimensions; + } + + private int computeScrollRangeWithSpanInfo(RecyclerView.State state) { + if (getChildCount() == 0 || state.getItemCount() == 0) { + return 0; + } + ensureLayoutState(); + + View startChild = findFirstVisibleChildClosestToStart(!isSmoothScrollbarEnabled(), true); + View endChild = findFirstVisibleChildClosestToEnd(!isSmoothScrollbarEnabled(), true); + + if (startChild == null || endChild == null) { + return 0; + } + if (!isSmoothScrollbarEnabled()) { + return mSpanSizeLookup.getCachedSpanGroupIndex( + state.getItemCount() - 1, mSpanCount) + 1; + } + + // smooth scrollbar enabled. try to estimate better. + final int laidOutArea = mOrientationHelper.getDecoratedEnd(endChild) + - mOrientationHelper.getDecoratedStart(startChild); + + final int firstVisibleSpan = + mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), mSpanCount); + final int lastVisibleSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild), + mSpanCount); + final int totalSpans = mSpanSizeLookup.getCachedSpanGroupIndex(state.getItemCount() - 1, + mSpanCount) + 1; + final int laidOutSpans = lastVisibleSpan - firstVisibleSpan + 1; + + // estimate a size for full list. + return (int) (((float) laidOutArea / laidOutSpans) * totalSpans); + } + + private int computeScrollOffsetWithSpanInfo(RecyclerView.State state) { + if (getChildCount() == 0 || state.getItemCount() == 0) { + return 0; + } + ensureLayoutState(); + + boolean smoothScrollEnabled = isSmoothScrollbarEnabled(); + View startChild = findFirstVisibleChildClosestToStart(!smoothScrollEnabled, true); + View endChild = findFirstVisibleChildClosestToEnd(!smoothScrollEnabled, true); + if (startChild == null || endChild == null) { + return 0; + } + int startChildSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), + mSpanCount); + int endChildSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild), + mSpanCount); + + final int minSpan = Math.min(startChildSpan, endChildSpan); + final int maxSpan = Math.max(startChildSpan, endChildSpan); + final int totalSpans = mSpanSizeLookup.getCachedSpanGroupIndex(state.getItemCount() - 1, + mSpanCount) + 1; + + final int spansBefore = mShouldReverseLayout + ? Math.max(0, totalSpans - maxSpan - 1) + : Math.max(0, minSpan); + if (!smoothScrollEnabled) { + return spansBefore; + } + final int laidOutArea = Math.abs(mOrientationHelper.getDecoratedEnd(endChild) + - mOrientationHelper.getDecoratedStart(startChild)); + + final int firstVisibleSpan = + mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), mSpanCount); + final int lastVisibleSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild), + mSpanCount); + final int laidOutSpans = lastVisibleSpan - firstVisibleSpan + 1; + final float avgSizePerSpan = (float) laidOutArea / laidOutSpans; + + return Math.round(spansBefore * avgSizePerSpan + (mOrientationHelper.getStartAfterPadding() + - mOrientationHelper.getDecoratedStart(startChild))); + } + /** * Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span. */ @@ -1396,7 +1456,7 @@ public static class LayoutParams extends RecyclerView.LayoutParams { int mSpanIndex = INVALID_SPAN_ID; - int mSpanSize; + int mSpanSize = 0; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); @@ -1446,5 +1506,4 @@ public int getSpanSize() { return mSpanSize; } } - -} +} \ No newline at end of file diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java b/viewpager2/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java index 052f6e4e8..4be404637 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java @@ -141,10 +141,15 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration */ @SuppressWarnings("WeakerAccess") public static final int ANIMATION_TYPE_DRAG = 1 << 3; - static final int DIRECTION_FLAG_COUNT = 8; + private static final String TAG = "ItemTouchHelper"; + private static final boolean DEBUG = false; + private static final int ACTIVE_POINTER_ID_NONE = -1; + + static final int DIRECTION_FLAG_COUNT = 8; + private static final int ACTION_MODE_IDLE_MASK = (1 << DIRECTION_FLAG_COUNT) - 1; static final int ACTION_MODE_SWIPE_MASK = ACTION_MODE_IDLE_MASK << DIRECTION_FLAG_COUNT; @@ -162,114 +167,271 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration * to clean these views. */ final List mPendingCleanup = new ArrayList<>(); - /** - * Developer callback which controls the behavior of ItemTouchHelper. - */ - @NonNull - final - Callback mCallback; - /** - * When a View is dragged or swiped and needs to go back to where it was, we create a Recover - * Animation and animate it to its location using this custom Animator, instead of using - * framework Animators. - * Using framework animators has the side effect of clashing with ItemAnimator, creating - * jumpy UIs. - */ - @VisibleForTesting - final - List mRecoverAnimations = new ArrayList<>(); + /** * Re-use array to calculate dx dy for a ViewHolder */ private final float[] mTmpPosition = new float[2]; + /** * Currently selected view holder */ @SuppressWarnings("WeakerAccess") /* synthetic access */ - ViewHolder mSelected; + ViewHolder mSelected = null; + /** * The reference coordinates for the action start. For drag & drop, this is the time long * press is completed vs for swipe, this is the initial touch point. */ float mInitialTouchX; + float mInitialTouchY; + + /** + * Set when ItemTouchHelper is assigned to a RecyclerView. + */ + private float mSwipeEscapeVelocity; + + /** + * Set when ItemTouchHelper is assigned to a RecyclerView. + */ + private float mMaxSwipeVelocity; + /** * The diff between the last event and initial touch. */ float mDx; + float mDy; + + /** + * The coordinates of the selected view at the time it is selected. We record these values + * when action starts so that we can consistently position it even if LayoutManager moves the + * View. + */ + private float mSelectedStartX; + + private float mSelectedStartY; + /** * The pointer we are tracking. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ - int mActivePointerId = ACTIVE_POINTER_ID_NONE; + int mActivePointerId = ACTIVE_POINTER_ID_NONE; + + /** + * Developer callback which controls the behavior of ItemTouchHelper. + */ + @NonNull + Callback mCallback; + + /** + * Current mode. + */ + private int mActionState = ACTION_STATE_IDLE; + /** * The direction flags obtained from unmasking * {@link Callback#getAbsoluteMovementFlags(RecyclerView, ViewHolder)} for the current * action state. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ - int mSelectedFlags; + int mSelectedFlags; + + /** + * When a View is dragged or swiped and needs to go back to where it was, we create a Recover + * Animation and animate it to its location using this custom Animator, instead of using + * framework Animators. + * Using framework animators has the side effect of clashing with ItemAnimator, creating + * jumpy UIs. + */ + @VisibleForTesting + List mRecoverAnimations = new ArrayList<>(); + + private int mSlop; + RecyclerView mRecyclerView; + + /** + * When user drags a view to the edge, we start scrolling the LayoutManager as long as View + * is partially out of bounds. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Runnable mScrollRunnable = new Runnable() { + @Override + public void run() { + if (mSelected != null && scrollIfNecessary()) { + if (mSelected != null) { //it might be lost during scrolling + moveIfNecessary(mSelected); + } + mRecyclerView.removeCallbacks(mScrollRunnable); + ViewCompat.postOnAnimation(mRecyclerView, this); + } + } + }; + /** * Used for detecting fling swipe */ VelocityTracker mVelocityTracker; + + //re-used list for selecting a swap target + private List mSwapTargets; + + //re used for for sorting swap targets + private List mDistances; + + /** + * If drag & drop is supported, we use child drawing order to bring them to front. + */ + private RecyclerView.ChildDrawingOrderCallback mChildDrawingOrderCallback = null; + /** * This keeps a reference to the child dragged by the user. Even after user stops dragging, * until view reaches its final position (end of recover animation), we keep a reference so * that it can be drawn above other children. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ - View mOverdrawChild; + View mOverdrawChild = null; + /** * We cache the position of the overdraw child to avoid recalculating it each time child * position callback is called. This value is invalidated whenever a child is attached or * detached. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ - int mOverdrawChildPosition = -1; + int mOverdrawChildPosition = -1; + /** * Used to detect long press. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ - GestureDetectorCompat mGestureDetector; - /** - * Set when ItemTouchHelper is assigned to a RecyclerView. - */ - private float mSwipeEscapeVelocity; - /** - * Set when ItemTouchHelper is assigned to a RecyclerView. - */ - private float mMaxSwipeVelocity; - /** - * The coordinates of the selected view at the time it is selected. We record these values - * when action starts so that we can consistently position it even if LayoutManager moves the - * View. - */ - private float mSelectedStartX; - private float mSelectedStartY; - /** - * Current mode. - */ - private int mActionState = ACTION_STATE_IDLE; - private int mSlop; - //re-used list for selecting a swap target - private List mSwapTargets; - //re used for for sorting swap targets - private List mDistances; - /** - * If drag & drop is supported, we use child drawing order to bring them to front. - */ - private RecyclerView.ChildDrawingOrderCallback mChildDrawingOrderCallback; + GestureDetectorCompat mGestureDetector; + /** * Callback for when long press occurs. */ private ItemTouchHelperGestureListener mItemTouchHelperGestureListener; + + private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() { + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, + @NonNull MotionEvent event) { + mGestureDetector.onTouchEvent(event); + if (DEBUG) { + Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event); + } + final int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN) { + mActivePointerId = event.getPointerId(0); + mInitialTouchX = event.getX(); + mInitialTouchY = event.getY(); + obtainVelocityTracker(); + if (mSelected == null) { + final RecoverAnimation animation = findAnimation(event); + if (animation != null) { + mInitialTouchX -= animation.mX; + mInitialTouchY -= animation.mY; + endRecoverAnimation(animation.mViewHolder, true); + if (mPendingCleanup.remove(animation.mViewHolder.itemView)) { + mCallback.clearView(mRecyclerView, animation.mViewHolder); + } + select(animation.mViewHolder, animation.mActionState); + updateDxDy(event, mSelectedFlags, 0); + } + } + } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + mActivePointerId = ACTIVE_POINTER_ID_NONE; + select(null, ACTION_STATE_IDLE); + } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) { + // in a non scroll orientation, if distance change is above threshold, we + // can select the item + final int index = event.findPointerIndex(mActivePointerId); + if (DEBUG) { + Log.d(TAG, "pointer index " + index); + } + if (index >= 0) { + checkSelectForSwipe(action, event, index); + } + } + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(event); + } + return mSelected != null; + } + + @Override + public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) { + mGestureDetector.onTouchEvent(event); + if (DEBUG) { + Log.d(TAG, + "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event); + } + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(event); + } + if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { + return; + } + final int action = event.getActionMasked(); + final int activePointerIndex = event.findPointerIndex(mActivePointerId); + if (activePointerIndex >= 0) { + checkSelectForSwipe(action, event, activePointerIndex); + } + ViewHolder viewHolder = mSelected; + if (viewHolder == null) { + return; + } + switch (action) { + case MotionEvent.ACTION_MOVE: { + // Find the index of the active pointer and fetch its position + if (activePointerIndex >= 0) { + updateDxDy(event, mSelectedFlags, activePointerIndex); + moveIfNecessary(viewHolder); + mRecyclerView.removeCallbacks(mScrollRunnable); + mScrollRunnable.run(); + mRecyclerView.invalidate(); + } + break; + } + case MotionEvent.ACTION_CANCEL: + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + // fall through + case MotionEvent.ACTION_UP: + select(null, ACTION_STATE_IDLE); + mActivePointerId = ACTIVE_POINTER_ID_NONE; + break; + case MotionEvent.ACTION_POINTER_UP: { + final int pointerIndex = event.getActionIndex(); + final int pointerId = event.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = event.getPointerId(newPointerIndex); + updateDxDy(event, mSelectedFlags, pointerIndex); + } + break; + } + } + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (!disallowIntercept) { + return; + } + select(null, ACTION_STATE_IDLE); + } + }; + /** * Temporary rect instance that is used when we need to lookup Item decorations. */ private Rect mTmpRect; + /** * When user started to drag scroll. Reset when we don't scroll */ @@ -313,7 +475,7 @@ public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { } mRecyclerView = recyclerView; if (recyclerView != null) { - Resources resources = recyclerView.getResources(); + final Resources resources = recyclerView.getResources(); mSwipeEscapeVelocity = resources .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity); mMaxSwipeVelocity = resources @@ -336,9 +498,9 @@ private void destroyCallbacks() { mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); mRecyclerView.removeOnChildAttachStateChangeListener(this); // clean all attached - int recoverAnimSize = mRecoverAnimations.size(); + final int recoverAnimSize = mRecoverAnimations.size(); for (int i = recoverAnimSize - 1; i >= 0; i--) { - RecoverAnimation recoverAnimation = mRecoverAnimations.get(0); + final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0); recoverAnimation.cancel(); mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder); } @@ -363,23 +525,7 @@ private void stopGestureDetection() { if (mGestureDetector != null) { mGestureDetector = null; } - } /** - * When user drags a view to the edge, we start scrolling the LayoutManager as long as View - * is partially out of bounds. - */ - @SuppressWarnings("WeakerAccess") /* synthetic access */ - final Runnable mScrollRunnable = new Runnable() { - @Override - public void run() { - if (mSelected != null && scrollIfNecessary()) { - if (mSelected != null) { //it might be lost during scrolling - moveIfNecessary(mSelected); - } - mRecyclerView.removeCallbacks(mScrollRunnable); - ViewCompat.postOnAnimation(mRecyclerView, this); - } - } - }; + } private void getSelectedDxDy(float[] outPosition) { if ((mSelectedFlags & (LEFT | RIGHT)) != 0) { @@ -438,7 +584,7 @@ void select(@Nullable ViewHolder selected, int actionState) { return; } mDragScrollStartTimeInMs = Long.MIN_VALUE; - int prevActionState = mActionState; + final int prevActionState = mActionState; // prevent duplicate animations endRecoverAnimation(selected, true); mActionState = actionState; @@ -458,13 +604,13 @@ void select(@Nullable ViewHolder selected, int actionState) { boolean preventLayout = false; if (mSelected != null) { - ViewHolder prevSelected = mSelected; + final ViewHolder prevSelected = mSelected; if (prevSelected.itemView.getParent() != null) { - int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0 + final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0 : swipeIfNecessary(prevSelected); releaseVelocityTracker(); // find where we should animate to - float targetTranslateX, targetTranslateY; + final float targetTranslateX, targetTranslateY; int animationType; switch (swipeDir) { case LEFT: @@ -491,15 +637,15 @@ void select(@Nullable ViewHolder selected, int actionState) { animationType = ANIMATION_TYPE_SWIPE_CANCEL; } getSelectedDxDy(mTmpPosition); - float currentTranslateX = mTmpPosition[0]; - float currentTranslateY = mTmpPosition[1]; - RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType, + final float currentTranslateX = mTmpPosition[0]; + final float currentTranslateY = mTmpPosition[1]; + final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType, prevActionState, currentTranslateX, currentTranslateY, targetTranslateX, targetTranslateY) { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); - if (mOverridden) { + if (this.mOverridden) { return; } if (swipeDir <= 0) { @@ -522,7 +668,7 @@ public void onAnimationEnd(Animator animation) { } } }; - long duration = mCallback.getAnimationDuration(mRecyclerView, animationType, + final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType, targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY); rv.setDuration(duration); mRecoverAnimations.add(rv); @@ -546,7 +692,7 @@ public void onAnimationEnd(Animator animation) { mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } } - ViewParent rvParent = mRecyclerView.getParent(); + final ViewParent rvParent = mRecyclerView.getParent(); if (rvParent != null) { rvParent.requestDisallowInterceptTouchEvent(mSelected != null); } @@ -558,7 +704,7 @@ public void onAnimationEnd(Animator animation) { } @SuppressWarnings("WeakerAccess") /* synthetic access */ - void postDispatchSwipe(RecoverAnimation anim, int swipeDir) { + void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) { // wait until animations are complete. mRecyclerView.post(new Runnable() { @Override @@ -567,7 +713,7 @@ public void run() { && !anim.mOverridden && anim.mViewHolder.getAbsoluteAdapterPosition() != RecyclerView.NO_POSITION) { - RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator(); + final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator(); // if animator is running or we have other active recover animations, we try // not to call onSwiped because DefaultItemAnimator is not good at merging // animations. Instead, we wait and batch. @@ -584,7 +730,7 @@ public void run() { @SuppressWarnings("WeakerAccess") /* synthetic access */ boolean hasRunningRecoverAnim() { - int size = mRecoverAnimations.size(); + final int size = mRecoverAnimations.size(); for (int i = 0; i < size; i++) { if (!mRecoverAnimations.get(i).mEnded) { return true; @@ -602,8 +748,8 @@ boolean scrollIfNecessary() { mDragScrollStartTimeInMs = Long.MIN_VALUE; return false; } - long now = System.currentTimeMillis(); - long scrollDuration = mDragScrollStartTimeInMs + final long now = System.currentTimeMillis(); + final long scrollDuration = mDragScrollStartTimeInMs == Long.MIN_VALUE ? 0 : now - mDragScrollStartTimeInMs; RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); if (mTmpRect == null) { @@ -614,11 +760,11 @@ boolean scrollIfNecessary() { lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect); if (lm.canScrollHorizontally()) { int curX = (int) (mSelectedStartX + mDx); - int leftDiff = curX - mTmpRect.left - mRecyclerView.getPaddingLeft(); + final int leftDiff = curX - mTmpRect.left - mRecyclerView.getPaddingLeft(); if (mDx < 0 && leftDiff < 0) { scrollX = leftDiff; } else if (mDx > 0) { - int rightDiff = + final int rightDiff = curX + mSelected.itemView.getWidth() + mTmpRect.right - (mRecyclerView.getWidth() - mRecyclerView.getPaddingRight()); if (rightDiff > 0) { @@ -628,11 +774,11 @@ boolean scrollIfNecessary() { } if (lm.canScrollVertically()) { int curY = (int) (mSelectedStartY + mDy); - int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop(); + final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop(); if (mDy < 0 && topDiff < 0) { scrollY = topDiff; } else if (mDy > 0) { - int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom + final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom - (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom()); if (bottomDiff > 0) { scrollY = bottomDiff; @@ -668,15 +814,15 @@ private List findSwapTargets(ViewHolder viewHolder) { mSwapTargets.clear(); mDistances.clear(); } - int margin = mCallback.getBoundingBoxMargin(); - int left = Math.round(mSelectedStartX + mDx) - margin; - int top = Math.round(mSelectedStartY + mDy) - margin; - int right = left + viewHolder.itemView.getWidth() + 2 * margin; - int bottom = top + viewHolder.itemView.getHeight() + 2 * margin; - int centerX = (left + right) / 2; - int centerY = (top + bottom) / 2; - RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); - int childCount = lm.getChildCount(); + final int margin = mCallback.getBoundingBoxMargin(); + final int left = Math.round(mSelectedStartX + mDx) - margin; + final int top = Math.round(mSelectedStartY + mDy) - margin; + final int right = left + viewHolder.itemView.getWidth() + 2 * margin; + final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin; + final int centerX = (left + right) / 2; + final int centerY = (top + bottom) / 2; + final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); + final int childCount = lm.getChildCount(); for (int i = 0; i < childCount; i++) { View other = lm.getChildAt(i); if (other == viewHolder.itemView) { @@ -686,15 +832,15 @@ private List findSwapTargets(ViewHolder viewHolder) { || other.getRight() < left || other.getLeft() > right) { continue; } - ViewHolder otherVh = mRecyclerView.getChildViewHolder(other); + final ViewHolder otherVh = mRecyclerView.getChildViewHolder(other); if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) { // find the index to add - int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2); - int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2); - int dist = dx * dx + dy * dy; + final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2); + final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2); + final int dist = dx * dx + dy * dy; int pos = 0; - int cnt = mSwapTargets.size(); + final int cnt = mSwapTargets.size(); for (int j = 0; j < cnt; j++) { if (dist > mDistances.get(j)) { pos++; @@ -721,9 +867,9 @@ void moveIfNecessary(ViewHolder viewHolder) { return; } - float threshold = mCallback.getMoveThreshold(viewHolder); - int x = (int) (mSelectedStartX + mDx); - int y = (int) (mSelectedStartY + mDy); + final float threshold = mCallback.getMoveThreshold(viewHolder); + final int x = (int) (mSelectedStartX + mDx); + final int y = (int) (mSelectedStartY + mDy); if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold && Math.abs(x - viewHolder.itemView.getLeft()) < viewHolder.itemView.getWidth() * threshold) { @@ -740,8 +886,8 @@ void moveIfNecessary(ViewHolder viewHolder) { mDistances.clear(); return; } - int toPosition = target.getAbsoluteAdapterPosition(); - int fromPosition = viewHolder.getAbsoluteAdapterPosition(); + final int toPosition = target.getAbsoluteAdapterPosition(); + final int fromPosition = viewHolder.getAbsoluteAdapterPosition(); if (mCallback.onMove(mRecyclerView, viewHolder, target)) { // keep target visible mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, @@ -756,7 +902,7 @@ public void onChildViewAttachedToWindow(@NonNull View view) { @Override public void onChildViewDetachedFromWindow(@NonNull View view) { removeChildDrawingOrderCallbackIfNecessary(view); - ViewHolder holder = mRecyclerView.getChildViewHolder(view); + final ViewHolder holder = mRecyclerView.getChildViewHolder(view); if (holder == null) { return; } @@ -775,9 +921,9 @@ public void onChildViewDetachedFromWindow(@NonNull View view) { */ @SuppressWarnings("WeakerAccess") /* synthetic access */ void endRecoverAnimation(ViewHolder viewHolder, boolean override) { - int recoverAnimSize = mRecoverAnimations.size(); + final int recoverAnimSize = mRecoverAnimations.size(); for (int i = recoverAnimSize - 1; i >= 0; i--) { - RecoverAnimation anim = mRecoverAnimations.get(i); + final RecoverAnimation anim = mRecoverAnimations.get(i); if (anim.mViewHolder == viewHolder) { anim.mOverridden |= override; if (!anim.mEnded) { @@ -792,7 +938,7 @@ void endRecoverAnimation(ViewHolder viewHolder, boolean override) { @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public void getItemOffsets(Rect outRect, View view, RecyclerView parent, - RecyclerView.State state) { + RecyclerView.State state) { outRect.setEmpty(); } @@ -812,15 +958,15 @@ private void releaseVelocityTracker() { } private ViewHolder findSwipedView(MotionEvent motionEvent) { - RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); + final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { return null; } - int pointerIndex = motionEvent.findPointerIndex(mActivePointerId); - float dx = motionEvent.getX(pointerIndex) - mInitialTouchX; - float dy = motionEvent.getY(pointerIndex) - mInitialTouchY; - float absDx = Math.abs(dx); - float absDy = Math.abs(dy); + final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId); + final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX; + final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY; + final float absDx = Math.abs(dx); + final float absDy = Math.abs(dy); if (absDx < mSlop && absDy < mSlop) { return null; @@ -849,13 +995,13 @@ void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { return; } - ViewHolder vh = findSwipedView(motionEvent); + final ViewHolder vh = findSwipedView(motionEvent); if (vh == null) { return; } - int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh); + final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh); - int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) + final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE); if (swipeFlags == 0) { @@ -864,16 +1010,16 @@ void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) // mDx and mDy are only set in allowed directions. We use custom x/y here instead of // updateDxDy to avoid swiping if user moves more in the other direction - float x = motionEvent.getX(pointerIndex); - float y = motionEvent.getY(pointerIndex); + final float x = motionEvent.getX(pointerIndex); + final float y = motionEvent.getY(pointerIndex); // Calculate the distance moved - float dx = x - mInitialTouchX; - float dy = y - mInitialTouchY; + final float dx = x - mInitialTouchX; + final float dy = y - mInitialTouchY; // swipe target is chose w/o applying flags so it does not really check if swiping in that // direction is allowed. This why here, we use mDx mDy to check slope value again. - float absDx = Math.abs(dx); - float absDy = Math.abs(dy); + final float absDx = Math.abs(dx); + final float absDy = Math.abs(dy); if (absDx < mSlop && absDy < mSlop) { return; @@ -901,17 +1047,17 @@ void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) @SuppressWarnings("WeakerAccess") /* synthetic access */ View findChildView(MotionEvent event) { // first check elevated views, if none, then call RV - float x = event.getX(); - float y = event.getY(); + final float x = event.getX(); + final float y = event.getY(); if (mSelected != null) { - View selectedView = mSelected.itemView; + final View selectedView = mSelected.itemView; if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) { return selectedView; } } for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { - RecoverAnimation anim = mRecoverAnimations.get(i); - View view = anim.mViewHolder.itemView; + final RecoverAnimation anim = mRecoverAnimations.get(i); + final View view = anim.mViewHolder.itemView; if (hitTest(view, x, y, anim.mX, anim.mY)) { return view; } @@ -934,7 +1080,7 @@ View findChildView(MotionEvent event) { * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener * grabs previous events, this should work as expected. * - *

+ * * For example, if you would like to let your user to be able to drag an Item by touching one * of its descendants, you may implement it as follows: *

@@ -983,7 +1129,7 @@ public void startDrag(@NonNull ViewHolder viewHolder) {
      * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener
      * grabs previous events, this should work as expected.
      * 
-     * 

+ * * For example, if you would like to let your user to be able to swipe an Item by touching one * of its descendants, you may implement it as follows: *

@@ -1022,7 +1168,7 @@ RecoverAnimation findAnimation(MotionEvent event) {
         }
         View target = findChildView(event);
         for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
-            RecoverAnimation anim = mRecoverAnimations.get(i);
+            final RecoverAnimation anim = mRecoverAnimations.get(i);
             if (anim.mViewHolder.itemView == target) {
                 return anim;
             }
@@ -1032,8 +1178,8 @@ RecoverAnimation findAnimation(MotionEvent event) {
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) {
-        float x = ev.getX(pointerIndex);
-        float y = ev.getY(pointerIndex);
+        final float x = ev.getX(pointerIndex);
+        final float y = ev.getY(pointerIndex);
 
         // Calculate the distance moved
         mDx = x - mInitialTouchX;
@@ -1056,16 +1202,16 @@ private int swipeIfNecessary(ViewHolder viewHolder) {
         if (mActionState == ACTION_STATE_DRAG) {
             return 0;
         }
-        int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder);
-        int absoluteMovementFlags = mCallback.convertToAbsoluteDirection(
+        final int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder);
+        final int absoluteMovementFlags = mCallback.convertToAbsoluteDirection(
                 originalMovementFlags,
                 ViewCompat.getLayoutDirection(mRecyclerView));
-        int flags = (absoluteMovementFlags
+        final int flags = (absoluteMovementFlags
                 & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT);
         if (flags == 0) {
             return 0;
         }
-        int originalFlags = (originalMovementFlags
+        final int originalFlags = (originalMovementFlags
                 & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT);
         int swipeDir;
         if (Math.abs(mDx) > Math.abs(mDy)) {
@@ -1100,14 +1246,14 @@ private int swipeIfNecessary(ViewHolder viewHolder) {
 
     private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) {
         if ((flags & (LEFT | RIGHT)) != 0) {
-            int dirFlag = mDx > 0 ? RIGHT : LEFT;
+            final int dirFlag = mDx > 0 ? RIGHT : LEFT;
             if (mVelocityTracker != null && mActivePointerId > -1) {
                 mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND,
                         mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity));
-                float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId);
-                float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId);
-                int velDirFlag = xVelocity > 0f ? RIGHT : LEFT;
-                float absXVelocity = Math.abs(xVelocity);
+                final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId);
+                final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId);
+                final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT;
+                final float absXVelocity = Math.abs(xVelocity);
                 if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag
                         && absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity)
                         && absXVelocity > Math.abs(yVelocity)) {
@@ -1115,7 +1261,7 @@ private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) {
                 }
             }
 
-            float threshold = mRecyclerView.getWidth() * mCallback
+            final float threshold = mRecyclerView.getWidth() * mCallback
                     .getSwipeThreshold(viewHolder);
 
             if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) {
@@ -1127,14 +1273,14 @@ private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) {
 
     private int checkVerticalSwipe(ViewHolder viewHolder, int flags) {
         if ((flags & (UP | DOWN)) != 0) {
-            int dirFlag = mDy > 0 ? DOWN : UP;
+            final int dirFlag = mDy > 0 ? DOWN : UP;
             if (mVelocityTracker != null && mActivePointerId > -1) {
                 mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND,
                         mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity));
-                float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId);
-                float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId);
-                int velDirFlag = yVelocity > 0f ? DOWN : UP;
-                float absYVelocity = Math.abs(yVelocity);
+                final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId);
+                final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId);
+                final int velDirFlag = yVelocity > 0f ? DOWN : UP;
+                final float absYVelocity = Math.abs(yVelocity);
                 if ((velDirFlag & flags) != 0 && velDirFlag == dirFlag
                         && absYVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity)
                         && absYVelocity > Math.abs(xVelocity)) {
@@ -1142,126 +1288,14 @@ private int checkVerticalSwipe(ViewHolder viewHolder, int flags) {
                 }
             }
 
-            float threshold = mRecyclerView.getHeight() * mCallback
+            final float threshold = mRecyclerView.getHeight() * mCallback
                     .getSwipeThreshold(viewHolder);
             if ((flags & dirFlag) != 0 && Math.abs(mDy) > threshold) {
                 return dirFlag;
             }
         }
         return 0;
-    }    private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
-        @Override
-        public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
-                                             @NonNull MotionEvent event) {
-            mGestureDetector.onTouchEvent(event);
-            if (DEBUG) {
-                Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);
-            }
-            int action = event.getActionMasked();
-            if (action == MotionEvent.ACTION_DOWN) {
-                mActivePointerId = event.getPointerId(0);
-                mInitialTouchX = event.getX();
-                mInitialTouchY = event.getY();
-                obtainVelocityTracker();
-                if (mSelected == null) {
-                    RecoverAnimation animation = findAnimation(event);
-                    if (animation != null) {
-                        mInitialTouchX -= animation.mX;
-                        mInitialTouchY -= animation.mY;
-                        endRecoverAnimation(animation.mViewHolder, true);
-                        if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
-                            mCallback.clearView(mRecyclerView, animation.mViewHolder);
-                        }
-                        select(animation.mViewHolder, animation.mActionState);
-                        updateDxDy(event, mSelectedFlags, 0);
-                    }
-                }
-            } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
-                mActivePointerId = ACTIVE_POINTER_ID_NONE;
-                select(null, ACTION_STATE_IDLE);
-            } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
-                // in a non scroll orientation, if distance change is above threshold, we
-                // can select the item
-                int index = event.findPointerIndex(mActivePointerId);
-                if (DEBUG) {
-                    Log.d(TAG, "pointer index " + index);
-                }
-                if (index >= 0) {
-                    checkSelectForSwipe(action, event, index);
-                }
-            }
-            if (mVelocityTracker != null) {
-                mVelocityTracker.addMovement(event);
-            }
-            return mSelected != null;
-        }
-
-        @Override
-        public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
-            mGestureDetector.onTouchEvent(event);
-            if (DEBUG) {
-                Log.d(TAG,
-                        "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event);
-            }
-            if (mVelocityTracker != null) {
-                mVelocityTracker.addMovement(event);
-            }
-            if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
-                return;
-            }
-            int action = event.getActionMasked();
-            int activePointerIndex = event.findPointerIndex(mActivePointerId);
-            if (activePointerIndex >= 0) {
-                checkSelectForSwipe(action, event, activePointerIndex);
-            }
-            ViewHolder viewHolder = mSelected;
-            if (viewHolder == null) {
-                return;
-            }
-            switch (action) {
-                case MotionEvent.ACTION_MOVE: {
-                    // Find the index of the active pointer and fetch its position
-                    if (activePointerIndex >= 0) {
-                        updateDxDy(event, mSelectedFlags, activePointerIndex);
-                        moveIfNecessary(viewHolder);
-                        mRecyclerView.removeCallbacks(mScrollRunnable);
-                        mScrollRunnable.run();
-                        mRecyclerView.invalidate();
-                    }
-                    break;
-                }
-                case MotionEvent.ACTION_CANCEL:
-                    if (mVelocityTracker != null) {
-                        mVelocityTracker.clear();
-                    }
-                    // fall through
-                case MotionEvent.ACTION_UP:
-                    select(null, ACTION_STATE_IDLE);
-                    mActivePointerId = ACTIVE_POINTER_ID_NONE;
-                    break;
-                case MotionEvent.ACTION_POINTER_UP: {
-                    int pointerIndex = event.getActionIndex();
-                    int pointerId = event.getPointerId(pointerIndex);
-                    if (pointerId == mActivePointerId) {
-                        // This was our active pointer going up. Choose a new
-                        // active pointer and adjust accordingly.
-                        int newPointerIndex = pointerIndex == 0 ? 1 : 0;
-                        mActivePointerId = event.getPointerId(newPointerIndex);
-                        updateDxDy(event, mSelectedFlags, pointerIndex);
-                    }
-                    break;
-                }
-            }
-        }
-
-        @Override
-        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
-            if (!disallowIntercept) {
-                return;
-            }
-            select(null, ACTION_STATE_IDLE);
-        }
-    };
+    }
 
     private void addChildDrawingOrderCallback() {
         if (Build.VERSION.SDK_INT >= 21) {
@@ -1523,7 +1557,7 @@ public static int makeFlag(int actionState, int directions) {
          * @see #makeFlag(int, int)
          */
         public abstract int getMovementFlags(@NonNull RecyclerView recyclerView,
-                                             @NonNull ViewHolder viewHolder);
+                @NonNull ViewHolder viewHolder);
 
         /**
          * Converts a given set of flags to absolution direction which means {@link #START} and
@@ -1555,19 +1589,19 @@ public int convertToAbsoluteDirection(int flags, int layoutDirection) {
         }
 
         final int getAbsoluteMovementFlags(RecyclerView recyclerView,
-                                           ViewHolder viewHolder) {
-            int flags = getMovementFlags(recyclerView, viewHolder);
+                ViewHolder viewHolder) {
+            final int flags = getMovementFlags(recyclerView, viewHolder);
             return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView));
         }
 
         boolean hasDragFlag(RecyclerView recyclerView, ViewHolder viewHolder) {
-            int flags = getAbsoluteMovementFlags(recyclerView, viewHolder);
+            final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder);
             return (flags & ACTION_MODE_DRAG_MASK) != 0;
         }
 
         boolean hasSwipeFlag(RecyclerView recyclerView,
-                             ViewHolder viewHolder) {
-            int flags = getAbsoluteMovementFlags(recyclerView, viewHolder);
+                ViewHolder viewHolder) {
+            final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder);
             return (flags & ACTION_MODE_SWIPE_MASK) != 0;
         }
 
@@ -1588,7 +1622,7 @@ boolean hasSwipeFlag(RecyclerView recyclerView,
          */
         @SuppressWarnings("WeakerAccess")
         public boolean canDropOver(@NonNull RecyclerView recyclerView, @NonNull ViewHolder current,
-                                   @NonNull ViewHolder target) {
+                @NonNull ViewHolder target) {
             return true;
         }
 
@@ -1612,7 +1646,7 @@ public boolean canDropOver(@NonNull RecyclerView recyclerView, @NonNull ViewHold
          * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int)
          */
         public abstract boolean onMove(@NonNull RecyclerView recyclerView,
-                                       @NonNull ViewHolder viewHolder, @NonNull ViewHolder target);
+                @NonNull ViewHolder viewHolder, @NonNull ViewHolder target);
 
         /**
          * Returns whether ItemTouchHelper should start a drag and drop operation if an item is
@@ -1765,20 +1799,20 @@ public float getSwipeVelocityThreshold(float defaultValue) {
         @SuppressWarnings("WeakerAccess")
         @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
         public ViewHolder chooseDropTarget(@NonNull ViewHolder selected,
-                                           @NonNull List dropTargets, int curX, int curY) {
+                @NonNull List dropTargets, int curX, int curY) {
             int right = curX + selected.itemView.getWidth();
             int bottom = curY + selected.itemView.getHeight();
             ViewHolder winner = null;
             int winnerScore = -1;
-            int dx = curX - selected.itemView.getLeft();
-            int dy = curY - selected.itemView.getTop();
-            int targetsSize = dropTargets.size();
+            final int dx = curX - selected.itemView.getLeft();
+            final int dy = curY - selected.itemView.getTop();
+            final int targetsSize = dropTargets.size();
             for (int i = 0; i < targetsSize; i++) {
-                ViewHolder target = dropTargets.get(i);
+                final ViewHolder target = dropTargets.get(i);
                 if (dx > 0) {
                     int diff = target.itemView.getRight() - right;
                     if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) {
-                        int score = Math.abs(diff);
+                        final int score = Math.abs(diff);
                         if (score > winnerScore) {
                             winnerScore = score;
                             winner = target;
@@ -1788,7 +1822,7 @@ public ViewHolder chooseDropTarget(@NonNull ViewHolder selected,
                 if (dx < 0) {
                     int diff = target.itemView.getLeft() - curX;
                     if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) {
-                        int score = Math.abs(diff);
+                        final int score = Math.abs(diff);
                         if (score > winnerScore) {
                             winnerScore = score;
                             winner = target;
@@ -1798,7 +1832,7 @@ public ViewHolder chooseDropTarget(@NonNull ViewHolder selected,
                 if (dy < 0) {
                     int diff = target.itemView.getTop() - curY;
                     if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) {
-                        int score = Math.abs(diff);
+                        final int score = Math.abs(diff);
                         if (score > winnerScore) {
                             winnerScore = score;
                             winner = target;
@@ -1809,7 +1843,7 @@ public ViewHolder chooseDropTarget(@NonNull ViewHolder selected,
                 if (dy > 0) {
                     int diff = target.itemView.getBottom() - bottom;
                     if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) {
-                        int score = Math.abs(diff);
+                        final int score = Math.abs(diff);
                         if (score > winnerScore) {
                             winnerScore = score;
                             winner = target;
@@ -1884,7 +1918,7 @@ private int getMaxDragScroll(RecyclerView recyclerView) {
          * This method is responsible to give necessary hint to the LayoutManager so that it will
          * keep the View in visible area. For example, for LinearLayoutManager, this is as simple
          * as calling {@link LinearLayoutManager#scrollToPositionWithOffset(int, int)}.
-         * 

+ * * Default implementation calls {@link RecyclerView#scrollToPosition(int)} if the View's * new position is likely to be out of bounds. *

@@ -1905,10 +1939,10 @@ private int getMaxDragScroll(RecyclerView recyclerView) { * are applied. This value does not include margins added by * {@link RecyclerView.ItemDecoration}s. */ - public void onMoved(@NonNull RecyclerView recyclerView, - @NonNull ViewHolder viewHolder, int fromPos, @NonNull ViewHolder target, - int toPos, int x, int y) { - RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + public void onMoved(@NonNull final RecyclerView recyclerView, + @NonNull final ViewHolder viewHolder, int fromPos, @NonNull final ViewHolder target, + int toPos, int x, int y) { + final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof ViewDropHandler) { ((ViewDropHandler) layoutManager).prepareForDrop(viewHolder.itemView, target.itemView, x, y); @@ -1917,22 +1951,22 @@ public void onMoved(@NonNull RecyclerView recyclerView, // if layout manager cannot handle it, do some guesswork if (layoutManager.canScrollHorizontally()) { - int minLeft = layoutManager.getDecoratedLeft(target.itemView); + final int minLeft = layoutManager.getDecoratedLeft(target.itemView); if (minLeft <= recyclerView.getPaddingLeft()) { recyclerView.scrollToPosition(toPos); } - int maxRight = layoutManager.getDecoratedRight(target.itemView); + final int maxRight = layoutManager.getDecoratedRight(target.itemView); if (maxRight >= recyclerView.getWidth() - recyclerView.getPaddingRight()) { recyclerView.scrollToPosition(toPos); } } if (layoutManager.canScrollVertically()) { - int minTop = layoutManager.getDecoratedTop(target.itemView); + final int minTop = layoutManager.getDecoratedTop(target.itemView); if (minTop <= recyclerView.getPaddingTop()) { recyclerView.scrollToPosition(toPos); } - int maxBottom = layoutManager.getDecoratedBottom(target.itemView); + final int maxBottom = layoutManager.getDecoratedBottom(target.itemView); if (maxBottom >= recyclerView.getHeight() - recyclerView.getPaddingBottom()) { recyclerView.scrollToPosition(toPos); } @@ -1940,43 +1974,43 @@ public void onMoved(@NonNull RecyclerView recyclerView, } void onDraw(Canvas c, RecyclerView parent, ViewHolder selected, - List recoverAnimationList, - int actionState, float dX, float dY) { - int recoverAnimSize = recoverAnimationList.size(); + List recoverAnimationList, + int actionState, float dX, float dY) { + final int recoverAnimSize = recoverAnimationList.size(); for (int i = 0; i < recoverAnimSize; i++) { - ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); + final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); anim.update(); - int count = c.save(); + final int count = c.save(); onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, false); c.restoreToCount(count); } if (selected != null) { - int count = c.save(); + final int count = c.save(); onChildDraw(c, parent, selected, dX, dY, actionState, true); c.restoreToCount(count); } } void onDrawOver(Canvas c, RecyclerView parent, ViewHolder selected, - List recoverAnimationList, - int actionState, float dX, float dY) { - int recoverAnimSize = recoverAnimationList.size(); + List recoverAnimationList, + int actionState, float dX, float dY) { + final int recoverAnimSize = recoverAnimationList.size(); for (int i = 0; i < recoverAnimSize; i++) { - ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); - int count = c.save(); + final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); + final int count = c.save(); onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, false); c.restoreToCount(count); } if (selected != null) { - int count = c.save(); + final int count = c.save(); onChildDrawOver(c, parent, selected, dX, dY, actionState, true); c.restoreToCount(count); } boolean hasRunningAnimation = false; for (int i = recoverAnimSize - 1; i >= 0; i--) { - RecoverAnimation anim = recoverAnimationList.get(i); + final RecoverAnimation anim = recoverAnimationList.get(i); if (anim.mEnded && !anim.mIsPendingCleanup) { recoverAnimationList.remove(i); } else if (!anim.mEnded) { @@ -2033,8 +2067,8 @@ public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder vi * boolean) */ public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, - @NonNull ViewHolder viewHolder, - float dX, float dY, int actionState, boolean isCurrentlyActive) { + @NonNull ViewHolder viewHolder, + float dX, float dY, int actionState, boolean isCurrentlyActive) { ItemTouchUIUtilImpl.INSTANCE.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState, isCurrentlyActive); } @@ -2067,9 +2101,9 @@ public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, * boolean) */ public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - ViewHolder viewHolder, - float dX, float dY, int actionState, boolean isCurrentlyActive) { + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + ViewHolder viewHolder, + float dX, float dY, int actionState, boolean isCurrentlyActive) { ItemTouchUIUtilImpl.INSTANCE.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState, isCurrentlyActive); } @@ -2096,8 +2130,8 @@ public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerVie */ @SuppressWarnings("WeakerAccess") public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, - float animateDx, float animateDy) { - RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator(); + float animateDx, float animateDy) { + final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator(); if (itemAnimator == null) { return animationType == ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION : DEFAULT_SWIPE_ANIMATION_DURATION; @@ -2129,22 +2163,22 @@ public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animati */ @SuppressWarnings("WeakerAccess") public int interpolateOutOfBoundsScroll(@NonNull RecyclerView recyclerView, - int viewSize, int viewSizeOutOfBounds, - int totalSize, long msSinceStartScroll) { - int maxScroll = getMaxDragScroll(recyclerView); - int absOutOfBounds = Math.abs(viewSizeOutOfBounds); - int direction = (int) Math.signum(viewSizeOutOfBounds); + int viewSize, int viewSizeOutOfBounds, + int totalSize, long msSinceStartScroll) { + final int maxScroll = getMaxDragScroll(recyclerView); + final int absOutOfBounds = Math.abs(viewSizeOutOfBounds); + final int direction = (int) Math.signum(viewSizeOutOfBounds); // might be negative if other direction float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize); - int cappedScroll = (int) (direction * maxScroll + final int cappedScroll = (int) (direction * maxScroll * sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio)); - float timeRatio; + final float timeRatio; if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) { timeRatio = 1f; } else { timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS; } - int value = (int) (cappedScroll * sDragScrollInterpolator + final int value = (int) (cappedScroll * sDragScrollInterpolator .getInterpolation(timeRatio)); if (value == 0) { return viewSizeOutOfBounds > 0 ? 1 : -1; @@ -2236,7 +2270,7 @@ public void setDefaultDragDirs(@SuppressWarnings("unused") int defaultDragDirs) */ @SuppressWarnings("WeakerAccess") public int getSwipeDirs(@SuppressWarnings("unused") @NonNull RecyclerView recyclerView, - @NonNull @SuppressWarnings("unused") ViewHolder viewHolder) { + @NonNull @SuppressWarnings("unused") ViewHolder viewHolder) { return mDefaultSwipeDirs; } @@ -2251,18 +2285,88 @@ public int getSwipeDirs(@SuppressWarnings("unused") @NonNull RecyclerView recycl */ @SuppressWarnings("WeakerAccess") public int getDragDirs(@SuppressWarnings("unused") @NonNull RecyclerView recyclerView, - @SuppressWarnings("unused") @NonNull ViewHolder viewHolder) { + @SuppressWarnings("unused") @NonNull ViewHolder viewHolder) { return mDefaultDragDirs; } @Override public int getMovementFlags(@NonNull RecyclerView recyclerView, - @NonNull ViewHolder viewHolder) { + @NonNull ViewHolder viewHolder) { return makeMovementFlags(getDragDirs(recyclerView, viewHolder), getSwipeDirs(recyclerView, viewHolder)); } } + private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener { + + /** + * Whether to execute code in response to the the invoking of + * {@link ItemTouchHelperGestureListener#onLongPress(MotionEvent)}. + * + * It is necessary to control this here because + * {@link GestureDetector.SimpleOnGestureListener} can only be set on a + * {@link GestureDetector} in a GestureDetector's constructor, a GestureDetector will call + * onLongPress if an {@link MotionEvent#ACTION_DOWN} event is not followed by another event + * that would cancel it (like {@link MotionEvent#ACTION_UP} or + * {@link MotionEvent#ACTION_CANCEL}), the long press responding to the long press event + * needs to be cancellable to prevent unexpected behavior. + * + * @see #doNotReactToLongPress() + */ + private boolean mShouldReactToLongPress = true; + + ItemTouchHelperGestureListener() { + } + + /** + * Call to prevent executing code in response to + * {@link ItemTouchHelperGestureListener#onLongPress(MotionEvent)} being called. + */ + void doNotReactToLongPress() { + mShouldReactToLongPress = false; + } + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + if (!mShouldReactToLongPress) { + return; + } + View child = findChildView(e); + if (child != null) { + ViewHolder vh = mRecyclerView.getChildViewHolder(child); + if (vh != null) { + if (!mCallback.hasDragFlag(mRecyclerView, vh)) { + return; + } + int pointerId = e.getPointerId(0); + // Long press is deferred. + // Check w/ active pointer id to avoid selecting after motion + // event is canceled. + if (pointerId == mActivePointerId) { + final int index = e.findPointerIndex(mActivePointerId); + final float x = e.getX(index); + final float y = e.getY(index); + mInitialTouchX = x; + mInitialTouchY = y; + mDx = mDy = 0f; + if (DEBUG) { + Log.d(TAG, + "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY); + } + if (mCallback.isLongPressDragEnabled()) { + select(vh, ACTION_STATE_DRAG); + } + } + } + } + } + } + @VisibleForTesting static class RecoverAnimation implements Animator.AnimatorListener { @@ -2291,14 +2395,14 @@ static class RecoverAnimation implements Animator.AnimatorListener { // if user starts touching a recovering view, we put it into interaction mode again, // instantly. - boolean mOverridden; + boolean mOverridden = false; - boolean mEnded; + boolean mEnded = false; private float mFraction; RecoverAnimation(ViewHolder viewHolder, int animationType, - int actionState, float startDx, float startDy, float targetX, float targetY) { + int actionState, float startDx, float startDy, float targetX, float targetY) { mActionState = actionState; mAnimationType = animationType; mViewHolder = viewHolder; @@ -2371,80 +2475,4 @@ public void onAnimationRepeat(Animator animation) { } } - - private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener { - - /** - * Whether to execute code in response to the the invoking of - * {@link ItemTouchHelperGestureListener#onLongPress(MotionEvent)}. - *

- * It is necessary to control this here because - * {@link GestureDetector.SimpleOnGestureListener} can only be set on a - * {@link GestureDetector} in a GestureDetector's constructor, a GestureDetector will call - * onLongPress if an {@link MotionEvent#ACTION_DOWN} event is not followed by another event - * that would cancel it (like {@link MotionEvent#ACTION_UP} or - * {@link MotionEvent#ACTION_CANCEL}), the long press responding to the long press event - * needs to be cancellable to prevent unexpected behavior. - * - * @see #doNotReactToLongPress() - */ - private boolean mShouldReactToLongPress = true; - - ItemTouchHelperGestureListener() { - } - - /** - * Call to prevent executing code in response to - * {@link ItemTouchHelperGestureListener#onLongPress(MotionEvent)} being called. - */ - void doNotReactToLongPress() { - mShouldReactToLongPress = false; - } - - @Override - public boolean onDown(MotionEvent e) { - return true; - } - - @Override - public void onLongPress(MotionEvent e) { - if (!mShouldReactToLongPress) { - return; - } - View child = findChildView(e); - if (child != null) { - ViewHolder vh = mRecyclerView.getChildViewHolder(child); - if (vh != null) { - if (!mCallback.hasDragFlag(mRecyclerView, vh)) { - return; - } - int pointerId = e.getPointerId(0); - // Long press is deferred. - // Check w/ active pointer id to avoid selecting after motion - // event is canceled. - if (pointerId == mActivePointerId) { - int index = e.findPointerIndex(mActivePointerId); - float x = e.getX(index); - float y = e.getY(index); - mInitialTouchX = x; - mInitialTouchY = y; - mDx = mDy = 0f; - if (DEBUG) { - Log.d(TAG, - "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY); - } - if (mCallback.isLongPressDragEnabled()) { - select(vh, ACTION_STATE_DRAG); - } - } - } - } - } - } - - - - - - } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtil.java b/viewpager2/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtil.java index a363c402d..8f4f8f061 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtil.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtil.java @@ -39,33 +39,30 @@ public interface ItemTouchUIUtil { * The default implementation for {@link ItemTouchHelper.Callback#onChildDraw(Canvas, * RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)} */ - @SuppressLint("UnknownNullness") - // b/240775049: Cannot annotate properly + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly void onDraw(Canvas c, RecyclerView recyclerView, View view, - float dX, float dY, int actionState, boolean isCurrentlyActive); + float dX, float dY, int actionState, boolean isCurrentlyActive); /** * The default implementation for {@link ItemTouchHelper.Callback#onChildDrawOver(Canvas, * RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)} */ - @SuppressLint("UnknownNullness") - // b/240775049: Cannot annotate properly + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly void onDrawOver(Canvas c, RecyclerView recyclerView, View view, - float dX, float dY, int actionState, boolean isCurrentlyActive); + float dX, float dY, int actionState, boolean isCurrentlyActive); /** * The default implementation for {@link ItemTouchHelper.Callback#clearView(RecyclerView, * RecyclerView.ViewHolder)} */ - @SuppressLint("UnknownNullness") - // b/240775049: Cannot annotate properly + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly void clearView(View view); /** * The default implementation for {@link ItemTouchHelper.Callback#onSelectedChanged( - *RecyclerView.ViewHolder, int)} + * RecyclerView.ViewHolder, int)} */ - @SuppressLint("UnknownNullness") - // b/240775049: Cannot annotate properly + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly void onSelected(View view); } + diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtilImpl.java b/viewpager2/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtilImpl.java index 05f21f803..dff8088f3 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtilImpl.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtilImpl.java @@ -29,23 +29,7 @@ * public API, which is not desired in this case. */ class ItemTouchUIUtilImpl implements ItemTouchUIUtil { - static final ItemTouchUIUtil INSTANCE = new ItemTouchUIUtilImpl(); - - private static float findMaxElevation(RecyclerView recyclerView, View itemView) { - int childCount = recyclerView.getChildCount(); - float max = 0; - for (int i = 0; i < childCount; i++) { - View child = recyclerView.getChildAt(i); - if (child == itemView) { - continue; - } - float elevation = ViewCompat.getElevation(child); - if (elevation > max) { - max = elevation; - } - } - return max; - } + static final ItemTouchUIUtil INSTANCE = new ItemTouchUIUtilImpl(); @Override public void onDraw( @@ -73,6 +57,22 @@ public void onDraw( view.setTranslationY(dY); } + private static float findMaxElevation(RecyclerView recyclerView, View itemView) { + final int childCount = recyclerView.getChildCount(); + float max = 0; + for (int i = 0; i < childCount; i++) { + final View child = recyclerView.getChildAt(i); + if (child == itemView) { + continue; + } + final float elevation = ViewCompat.getElevation(child); + if (elevation > max) { + max = elevation; + } + } + return max; + } + @Override public void onDrawOver( @NonNull Canvas c, @@ -88,7 +88,7 @@ public void onDrawOver( @Override public void clearView(@NonNull View view) { if (Build.VERSION.SDK_INT >= 21) { - Object tag = view.getTag(R.id.item_touch_helper_previous_elevation); + final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation); if (tag instanceof Float) { ViewCompat.setElevation(view, (Float) tag); } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/LayoutState.java b/viewpager2/src/main/java/androidx/recyclerview/widget/LayoutState.java index fefc442ca..8805c1cc9 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/LayoutState.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/LayoutState.java @@ -18,8 +18,6 @@ import android.view.View; -import androidx.annotation.NonNull; - /** * Helper class that keeps temporary state while {LayoutManager} is filling out the empty * space. @@ -66,12 +64,12 @@ class LayoutState { /** * This is the target pixel closest to the start of the layout that we are trying to fill */ - int mStartLine; + int mStartLine = 0; /** * This is the target pixel closest to the end of the layout that we are trying to fill */ - int mEndLine; + int mEndLine = 0; /** * If true, layout should stop if a focusable view is added @@ -97,12 +95,11 @@ boolean hasMore(RecyclerView.State state) { * @return The next element that we should render. */ View next(RecyclerView.Recycler recycler) { - View view = recycler.getViewForPosition(mCurrentPosition); + final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; } - @NonNull @Override public String toString() { return "LayoutState{" diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/viewpager2/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java index 37ec285ee..74350de23 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java @@ -19,15 +19,21 @@ import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PointF; +import android.os.Build; +import android.os.Bundle; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; +import android.widget.ListView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; import androidx.tracing.Trace; import java.util.List; @@ -39,90 +45,137 @@ public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider { + private static final String TAG = "LinearLayoutManager"; + + static final boolean DEBUG = false; + public static final int HORIZONTAL = RecyclerView.HORIZONTAL; + public static final int VERTICAL = RecyclerView.VERTICAL; + public static final int INVALID_OFFSET = Integer.MIN_VALUE; - static final boolean DEBUG = false; - private static final String TAG = "LinearLayoutManager"; + + /** * While trying to find next view to focus, LayoutManager will not try to scroll more * than this factor times the total space of the list. If layout is vertical, total space is the * height minus padding, if layout is horizontal, total space is the width minus padding. */ private static final float MAX_SCROLL_FACTOR = 1 / 3f; - /** - * Re-used variable to keep anchor information on re-layout. - * Anchor position and coordinate defines the reference point for LLM while doing a layout. - */ - final AnchorInfo mAnchorInfo = new AnchorInfo(); - /** - * Stashed to avoid allocation, currently only used in #fill() - */ - private final LayoutChunkResult mLayoutChunkResult = new LayoutChunkResult(); - // Reusable int array to be passed to method calls that mutate it in order to "return" two ints. - // This should only be used used transiently and should not be used to retain any state over - // time. - private final int[] mReusableIntPair = new int[2]; + /** * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL} */ @RecyclerView.Orientation int mOrientation = RecyclerView.DEFAULT_ORIENTATION; + + /** + * Helper class that keeps temporary layout state. + * It does not keep state after layout is complete but we still keep a reference to re-use + * the same object. + */ + private LayoutState mLayoutState; + /** * Many calculations are made depending on orientation. To keep it clean, this interface * helps {@link LinearLayoutManager} make those decisions. */ OrientationHelper mOrientationHelper; + /** - * This keeps the final value for how LayoutManager should start laying out views. - * It is calculated by checking {@link #getReverseLayout()} and View's layout direction. - * {@link #onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)} is run. - */ - boolean mShouldReverseLayout; - /** - * When LayoutManager needs to scroll to a position, it sets this variable and requests a - * layout which will check this variable and re-layout accordingly. - */ - int mPendingScrollPosition = RecyclerView.NO_POSITION; - /** - * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is - * called. + * We need to track this so that we can ignore current position when it changes. */ - int mPendingScrollPositionOffset = INVALID_OFFSET; - LinearLayoutManager_SavedState mPendingSavedState; + private boolean mLastStackFromEnd; + /** - * Helper class that keeps temporary layout state. - * It does not keep state after layout is complete but we still keep a reference to re-use - * the same object. + * Whether the last layout filled the entire viewport + * + * If the last layout did not fill the viewport, we should not attempt to calculate an + * anchoring based on the current children (other than if one is focused), because there + * isn't any scrolling that could have occurred that would indicate a position in the list + * that needs to be preserved - and in fact, trying to do so could produce the wrong result, + * such as the case of anchoring to a loading spinner at the end of the list. */ - private LayoutState mLayoutState; + private boolean mLastLayoutFilledViewport = false; + /** - * We need to track this so that we can ignore current position when it changes. + * Whether the *current* layout filled the entire viewport + * + * This is used to populate mLastLayoutFilledViewport. It exists as a separate variable + * because we need to populate it at the correct moment, which is tricky due to the + * LayoutManager layout being called multiple times. We want to not set it in prelayout + * (because that's not the real layout), but we want to set it the *first* time that the + * actual layout is run, because for certain non-exact layout cases, there are two passes, + * with the second pass being provided an EXACTLY spec (when the actual spec was non-exact). + * This would otherwise incorrectly believe the viewport was filled, because it was provided + * just enough space to contain the content, and thus it would always fill the viewport. */ - private boolean mLastStackFromEnd; + private Boolean mThisLayoutFilledViewport = null; + /** * Defines if layout should be calculated from end to start. * * @see #mShouldReverseLayout */ - private boolean mReverseLayout; + private boolean mReverseLayout = false; + + /** + * This keeps the final value for how LayoutManager should start laying out views. + * It is calculated by checking {@link #getReverseLayout()} and View's layout direction. + * {@link #onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)} is run. + */ + boolean mShouldReverseLayout = false; + /** * Works the same way as {@link android.widget.AbsListView#setStackFromBottom(boolean)} and * it supports both orientations. * see {@link android.widget.AbsListView#setStackFromBottom(boolean)} */ - private boolean mStackFromEnd; + private boolean mStackFromEnd = false; + /** * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} */ private boolean mSmoothScrollbarEnabled = true; + + /** + * When LayoutManager needs to scroll to a position, it sets this variable and requests a + * layout which will check this variable and re-layout accordingly. + */ + int mPendingScrollPosition = RecyclerView.NO_POSITION; + + /** + * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is + * called. + */ + int mPendingScrollPositionOffset = INVALID_OFFSET; + private boolean mRecycleChildrenOnDetach; + + LinearLayoutManager_SavedState mPendingSavedState = null; + + /** + * Re-used variable to keep anchor information on re-layout. + * Anchor position and coordinate defines the reference point for LLM while doing a layout. + */ + final AnchorInfo mAnchorInfo = new AnchorInfo(); + + /** + * Stashed to avoid allocation, currently only used in #fill() + */ + private final LayoutChunkResult mLayoutChunkResult = new LayoutChunkResult(); + /** * Number of items to prefetch when first coming on screen with new data. */ private int mInitialPrefetchItemCount = 2; + // Reusable int array to be passed to method calls that mutate it in order to "return" two ints. + // This should only be used used transiently and should not be used to retain any state over + // time. + private int[] mReusableIntPair = new int[2]; + /** * Creates a vertical LinearLayoutManager * @@ -156,14 +209,14 @@ public LinearLayoutManager( /** * Constructor used when layout manager is set in XML by RecyclerView attribute * "layoutManager". Defaults to vertical orientation. - *

+ * * {@link android.R.attr#orientation} - * {@link androidx.recyclerview.R.attr#reverseLayout} - * {@link androidx.recyclerview.R.attr#stackFromEnd} + * {@link androidx.viewpager2.R.attr#reverseLayout} + * {@link androidx.viewpager2.R.attr#stackFromEnd} */ @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { + int defStyleRes) { Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); setOrientation(properties.orientation); setReverseLayout(properties.reverseLayout); @@ -233,6 +286,60 @@ public void onInitializeAccessibilityEvent(AccessibilityEvent event) { } } + @Override + public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler, + @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(recycler, state, info); + // Set the class name so this is treated as a list. This helps accessibility services + // distinguish lists from one row or one column grids. + info.setClassName(ListView.class.getName()); + + // TODO(b/251823537) + if (mRecyclerView.mAdapter != null && mRecyclerView.mAdapter.getItemCount() > 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + info.addAction(AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION); + } + } + } + + @Override + boolean performAccessibilityAction(int action, @Nullable Bundle args) { + if (super.performAccessibilityAction(action, args)) { + return true; + } + + if (action == android.R.id.accessibilityActionScrollToPosition && args != null) { + int position = -1; + + if (mOrientation == VERTICAL) { + final int rowArg = args.getInt( + AccessibilityNodeInfoCompat.ACTION_ARGUMENT_ROW_INT, -1); + if (rowArg < 0) { + return false; + } + position = Math.min(rowArg, getRowCountForAccessibility(mRecyclerView.mRecycler, + mRecyclerView.mState) - 1); + } else { // horizontal + final int columnArg = args.getInt( + AccessibilityNodeInfoCompat.ACTION_ARGUMENT_COLUMN_INT, -1); + if (columnArg < 0) { + return false; + } + position = Math.min(columnArg, + getColumnCountForAccessibility(mRecyclerView.mRecycler, + mRecyclerView.mState) - 1); + } + if (position >= 0) { + // We want the target element to be the first on screen. That way, a + // screenreader like Talkback can directly focus on it as part of its default focus + // logic. + scrollToPositionWithOffset(position, 0); + return true; + } + } + return false; + } + @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public Parcelable onSaveInstanceState() { @@ -243,15 +350,17 @@ public Parcelable onSaveInstanceState() { if (getChildCount() > 0) { ensureLayoutState(); boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout; - state.setMAnchorLayoutFromEnd(didLayoutFromEnd); + state.mAnchorLayoutFromEnd = didLayoutFromEnd; if (didLayoutFromEnd) { - View refChild = getChildClosestToEnd(); - state.setMAnchorOffset(mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(refChild)); - state.setMAnchorPosition(getPosition(refChild)); + final View refChild = getChildClosestToEnd(); + state.mAnchorOffset = mOrientationHelper.getEndAfterPadding() + - mOrientationHelper.getDecoratedEnd(refChild); + state.mAnchorPosition = getPosition(refChild); } else { - View refChild = getChildClosestToStart(); - state.setMAnchorPosition(getPosition(refChild)); - state.setMAnchorOffset(mOrientationHelper.getDecoratedStart(refChild) - mOrientationHelper.getStartAfterPadding()); + final View refChild = getChildClosestToStart(); + state.mAnchorPosition = getPosition(refChild); + state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild) + - mOrientationHelper.getStartAfterPadding(); } } else { state.invalidateAnchor(); @@ -292,10 +401,6 @@ public boolean canScrollVertically() { return mOrientation == VERTICAL; } - public boolean getStackFromEnd() { - return mStackFromEnd; - } - /** * Compatibility support for {@link android.widget.AbsListView#setStackFromBottom(boolean)} */ @@ -308,6 +413,10 @@ public void setStackFromEnd(boolean stackFromEnd) { requestLayout(); } + public boolean getStackFromEnd() { + return mStackFromEnd; + } + /** * Returns the current orientation of the layout. * @@ -369,12 +478,12 @@ public boolean getReverseLayout() { * Used to reverse item traversal and layout order. * This behaves similar to the layout change for RTL views. When set to true, first item is * laid out at the end of the UI, second item is laid out before it etc. - *

+ * * For horizontal layouts, it depends on the layout direction. * When set to true, If {@link RecyclerView} is LTR, than it will * layout from RTL, if {@link RecyclerView}} is RTL, it will layout * from LTR. - *

+ * * If you are looking for the exact same behavior of * {@link android.widget.AbsListView#setStackFromBottom(boolean)}, use * {@link #setStackFromEnd(boolean)} @@ -394,14 +503,14 @@ public void setReverseLayout(boolean reverseLayout) { @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public View findViewByPosition(int position) { - int childCount = getChildCount(); + final int childCount = getChildCount(); if (childCount == 0) { return null; } - int firstChild = getPosition(getChildAt(0)); - int viewPosition = position - firstChild; + final int firstChild = getPosition(getChildAt(0)); + final int viewPosition = position - firstChild; if (viewPosition >= 0 && viewPosition < childCount) { - View child = getChildAt(viewPosition); + final View child = getChildAt(viewPosition); if (getPosition(child) == position) { return child; // in pre-layout, this may not match } @@ -433,7 +542,7 @@ public View findViewByPosition(int position) { */ @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated - protected int getExtraLayoutSpace(@SuppressLint("UnknownNullness") RecyclerView.State state) { + protected int getExtraLayoutSpace(RecyclerView.State state) { if (state.hasTargetScrollPosition()) { return mOrientationHelper.getTotalSpace(); } else { @@ -468,7 +577,7 @@ protected int getExtraLayoutSpace(@SuppressLint("UnknownNullness") RecyclerView. * unless the cache is large enough to handle it.

*/ protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state, - @NonNull int[] extraLayoutSpace) { + @NonNull int[] extraLayoutSpace) { int extraLayoutSpaceStart = 0; int extraLayoutSpaceEnd = 0; @@ -489,7 +598,7 @@ protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state, @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, - int position) { + int position) { LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()); linearSmoothScroller.setTargetPosition(position); @@ -502,8 +611,8 @@ public PointF computeScrollVectorForPosition(int targetPosition) { if (getChildCount() == 0) { return null; } - int firstChildPos = getPosition(getChildAt(0)); - int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1; + final int firstChildPos = getPosition(getChildAt(0)); + final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1; if (mOrientation == HORIZONTAL) { return new PointF(direction, 0); } else { @@ -534,7 +643,7 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State } } if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { - mPendingScrollPosition = mPendingSavedState.getMAnchorPosition(); + mPendingScrollPosition = mPendingSavedState.mAnchorPosition; } ensureLayoutState(); @@ -542,7 +651,7 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State // resolve layout direction resolveShouldLayoutReverse(); - View focused = getFocusedChild(); + final View focused = getFocusedChild(); if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION || mPendingSavedState != null) { mAnchorInfo.reset(); @@ -588,10 +697,10 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State // if the child is visible and we are going to move it around, we should layout // extra items in the opposite direction to make sure new items animate nicely // instead of just fading in - View existing = findViewByPosition(mPendingScrollPosition); + final View existing = findViewByPosition(mPendingScrollPosition); if (existing != null) { - int current; - int upcomingOffset; + final int current; + final int upcomingOffset; if (mShouldReverseLayout) { current = mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(existing); @@ -610,7 +719,7 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State } int startOffset; int endOffset; - int firstLayoutDirection; + final int firstLayoutDirection; if (mAnchorInfo.mLayoutFromEnd) { firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD; @@ -632,7 +741,7 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State mLayoutState.mExtraFillSpace = extraForStart; fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; - int firstElement = mLayoutState.mCurrentPosition; + final int firstElement = mLayoutState.mCurrentPosition; if (mLayoutState.mAvailable > 0) { extraForEnd += mLayoutState.mAvailable; } @@ -657,7 +766,7 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State mLayoutState.mExtraFillSpace = extraForEnd; fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; - int lastElement = mLayoutState.mCurrentPosition; + final int lastElement = mLayoutState.mCurrentPosition; if (mLayoutState.mAvailable > 0) { extraForStart += mLayoutState.mAvailable; } @@ -692,6 +801,10 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State fixOffset = fixLayoutStartGap(startOffset, recycler, state, false); startOffset += fixOffset; endOffset += fixOffset; + if (!state.isPreLayout() && mThisLayoutFilledViewport == null) { + mThisLayoutFilledViewport = + (startOffset <= mOrientationHelper.getStartAfterPadding()); + } } else { int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true); startOffset += fixOffset; @@ -699,6 +812,10 @@ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State fixOffset = fixLayoutEndGap(endOffset, recycler, state, false); startOffset += fixOffset; endOffset += fixOffset; + if (!state.isPreLayout() && mThisLayoutFilledViewport == null) { + mThisLayoutFilledViewport = + (endOffset >= mOrientationHelper.getEndAfterPadding()); + } } } layoutForPredictiveAnimations(recycler, state, startOffset, endOffset); @@ -720,6 +837,8 @@ public void onLayoutCompleted(RecyclerView.State state) { mPendingSavedState = null; // we don't need this anymore mPendingScrollPosition = RecyclerView.NO_POSITION; mPendingScrollPositionOffset = INVALID_OFFSET; + mLastLayoutFilledViewport = mThisLayoutFilledViewport != null && mThisLayoutFilledViewport; + mThisLayoutFilledViewport = null; mAnchorInfo.reset(); } @@ -734,15 +853,15 @@ public void onLayoutCompleted(RecyclerView.State state) { * indices. */ void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state, - AnchorInfo anchorInfo, int firstLayoutItemDirection) { + AnchorInfo anchorInfo, int firstLayoutItemDirection) { } /** * If necessary, layouts new items for predictive animations */ private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler, - RecyclerView.State state, int startOffset, - int endOffset) { + RecyclerView.State state, int startOffset, + int endOffset) { // If there are scrap children that we did not layout, we need to find where they did go // and layout them accordingly so that animations can work as expected. // This case may happen if new views are added or an existing view expands and pushes @@ -753,16 +872,16 @@ private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler, } // to make the logic simpler, we calculate the size of children and call fill. int scrapExtraStart = 0, scrapExtraEnd = 0; - List scrapList = recycler.getScrapList(); - int scrapSize = scrapList.size(); - int firstChildPos = getPosition(getChildAt(0)); + final List scrapList = recycler.getScrapList(); + final int scrapSize = scrapList.size(); + final int firstChildPos = getPosition(getChildAt(0)); for (int i = 0; i < scrapSize; i++) { RecyclerView.ViewHolder scrap = scrapList.get(i); if (scrap.isRemoved()) { continue; } - int position = scrap.getLayoutPosition(); - int direction = position < firstChildPos != mShouldReverseLayout + final int position = scrap.getLayoutPosition(); + final int direction = position < firstChildPos != mShouldReverseLayout ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; if (direction == LayoutState.LAYOUT_START) { scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); @@ -797,7 +916,7 @@ private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler, } private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, - AnchorInfo anchorInfo) { + AnchorInfo anchorInfo) { if (updateAnchorFromPendingData(state, anchorInfo)) { if (DEBUG) { Log.d(TAG, "updated anchor info from pending information"); @@ -825,15 +944,23 @@ private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerV * If a child has focus, it is given priority. */ private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, - RecyclerView.State state, AnchorInfo anchorInfo) { + RecyclerView.State state, AnchorInfo anchorInfo) { if (getChildCount() == 0) { return false; } - View focused = getFocusedChild(); + + final View focused = getFocusedChild(); if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) { anchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused)); return true; } + + // If we did not fill the layout, don't anchor. This prevents, for example, + // anchoring to the bottom of the list when there is a loading indicator. + if (!mLastLayoutFilledViewport) { + return false; + } + if (mLastStackFromEnd != mStackFromEnd) { return false; } @@ -849,10 +976,10 @@ private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, // If that is the case, offset it back to 0 so that we use these pre-layout children. if (!state.isPreLayout() && supportsPredictiveItemAnimations()) { // validate this child is at least partially visible. if not, offset it to start - int childStart = mOrientationHelper.getDecoratedStart(referenceChild); - int childEnd = mOrientationHelper.getDecoratedEnd(referenceChild); - int boundsStart = mOrientationHelper.getStartAfterPadding(); - int boundsEnd = mOrientationHelper.getEndAfterPadding(); + final int childStart = mOrientationHelper.getDecoratedStart(referenceChild); + final int childEnd = mOrientationHelper.getDecoratedEnd(referenceChild); + final int boundsStart = mOrientationHelper.getStartAfterPadding(); + final int boundsEnd = mOrientationHelper.getEndAfterPadding(); // b/148869110: usually if childStart >= boundsEnd the child is out of // bounds, except if the child is 0 pixels! boolean outOfBoundsBefore = childEnd <= boundsStart && childStart < boundsStart; @@ -890,13 +1017,13 @@ private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { // Anchor offset depends on how that child was laid out. Here, we update it // according to our current view bounds - anchorInfo.mLayoutFromEnd = mPendingSavedState.getMAnchorLayoutFromEnd(); + anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; if (anchorInfo.mLayoutFromEnd) { anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() - - mPendingSavedState.getMAnchorOffset(); + - mPendingSavedState.mAnchorOffset; } else { anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding() - + mPendingSavedState.getMAnchorOffset(); + + mPendingSavedState.mAnchorOffset; } return true; } @@ -904,20 +1031,20 @@ private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo if (mPendingScrollPositionOffset == INVALID_OFFSET) { View child = findViewByPosition(mPendingScrollPosition); if (child != null) { - int childSize = mOrientationHelper.getDecoratedMeasurement(child); + final int childSize = mOrientationHelper.getDecoratedMeasurement(child); if (childSize > mOrientationHelper.getTotalSpace()) { // item does not fit. fix depending on layout direction anchorInfo.assignCoordinateFromPadding(); return true; } - int startGap = mOrientationHelper.getDecoratedStart(child) + final int startGap = mOrientationHelper.getDecoratedStart(child) - mOrientationHelper.getStartAfterPadding(); if (startGap < 0) { anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding(); anchorInfo.mLayoutFromEnd = false; return true; } - int endGap = mOrientationHelper.getEndAfterPadding() + final int endGap = mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(child); if (endGap < 0) { anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding(); @@ -956,9 +1083,9 @@ private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo * @return The final offset amount for children */ private int fixLayoutEndGap(int endOffset, RecyclerView.Recycler recycler, - RecyclerView.State state, boolean canOffsetChildren) { + RecyclerView.State state, boolean canOffsetChildren) { int gap = mOrientationHelper.getEndAfterPadding() - endOffset; - int fixOffset; + int fixOffset = 0; if (gap > 0) { fixOffset = -scrollBy(-gap, recycler, state); } else { @@ -981,9 +1108,9 @@ private int fixLayoutEndGap(int endOffset, RecyclerView.Recycler recycler, * @return The final offset amount for children */ private int fixLayoutStartGap(int startOffset, RecyclerView.Recycler recycler, - RecyclerView.State state, boolean canOffsetChildren) { + RecyclerView.State state, boolean canOffsetChildren) { int gap = startOffset - mOrientationHelper.getStartAfterPadding(); - int fixOffset; + int fixOffset = 0; if (gap > 0) { // check if we should fix this gap. fixOffset = -scrollBy(gap, recycler, state); @@ -1109,7 +1236,7 @@ public void scrollToPositionWithOffset(int position, int offset) { @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, - RecyclerView.State state) { + RecyclerView.State state) { if (mOrientation == VERTICAL) { return 0; } @@ -1122,7 +1249,7 @@ public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, - RecyclerView.State state) { + RecyclerView.State state) { if (mOrientation == HORIZONTAL) { return 0; } @@ -1198,16 +1325,6 @@ private int computeScrollRange(RecyclerView.State state) { this, mSmoothScrollbarEnabled); } - /** - * Returns the current state of the smooth scrollbar feature. It is enabled by default. - * - * @return True if smooth scrollbar is enabled, false otherwise. - * @see #setSmoothScrollbarEnabled(boolean) - */ - public boolean isSmoothScrollbarEnabled() { - return mSmoothScrollbarEnabled; - } - /** * When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed * based on the number of visible pixels in the visible items. This however assumes that all @@ -1215,7 +1332,7 @@ public boolean isSmoothScrollbarEnabled() { * If you use a list in which items have different dimensions, the scrollbar will change * appearance as the user scrolls through the list. To avoid this issue, you need to disable * this property. - *

+ * * When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based * solely on the number of items in the adapter and the position of the visible items inside * the adapter. This provides a stable scrollbar as the user navigates through a list of items @@ -1228,8 +1345,18 @@ public void setSmoothScrollbarEnabled(boolean enabled) { mSmoothScrollbarEnabled = enabled; } + /** + * Returns the current state of the smooth scrollbar feature. It is enabled by default. + * + * @return True if smooth scrollbar is enabled, false otherwise. + * @see #setSmoothScrollbarEnabled(boolean) + */ + public boolean isSmoothScrollbarEnabled() { + return mSmoothScrollbarEnabled; + } + private void updateLayoutState(int layoutDirection, int requiredSpace, - boolean canUseExistingSpace, RecyclerView.State state) { + boolean canUseExistingSpace, RecyclerView.State state) { // If parent provides a hint, don't measure unlimited. mLayoutState.mInfinite = resolveIsInfinite(); mLayoutState.mLayoutDirection = layoutDirection; @@ -1245,7 +1372,7 @@ private void updateLayoutState(int layoutDirection, int requiredSpace, if (layoutToEnd) { mLayoutState.mExtraFillSpace += mOrientationHelper.getEndPadding(); // get the first child in the direction we are going - View child = getChildClosestToEnd(); + final View child = getChildClosestToEnd(); // the direction in which we are traversing children mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD : LayoutState.ITEM_DIRECTION_TAIL; @@ -1256,7 +1383,7 @@ private void updateLayoutState(int layoutDirection, int requiredSpace, - mOrientationHelper.getEndAfterPadding(); } else { - View child = getChildClosestToStart(); + final View child = getChildClosestToStart(); mLayoutState.mExtraFillSpace += mOrientationHelper.getStartAfterPadding(); mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD; @@ -1278,8 +1405,8 @@ boolean resolveIsInfinite() { } void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState, - LayoutPrefetchRegistry layoutPrefetchRegistry) { - int pos = layoutState.mCurrentPosition; + LayoutPrefetchRegistry layoutPrefetchRegistry) { + final int pos = layoutState.mCurrentPosition; if (pos >= 0 && pos < state.getItemCount()) { layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset)); } @@ -1288,13 +1415,13 @@ void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutStat @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public void collectInitialPrefetchPositions(int adapterItemCount, - LayoutPrefetchRegistry layoutPrefetchRegistry) { - boolean fromEnd; - int anchorPos; + LayoutPrefetchRegistry layoutPrefetchRegistry) { + final boolean fromEnd; + final int anchorPos; if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { // use restored state, since it hasn't been resolved yet - fromEnd = mPendingSavedState.getMAnchorLayoutFromEnd(); - anchorPos = mPendingSavedState.getMAnchorPosition(); + fromEnd = mPendingSavedState.mAnchorLayoutFromEnd; + anchorPos = mPendingSavedState.mAnchorPosition; } else { resolveShouldLayoutReverse(); fromEnd = mShouldReverseLayout; @@ -1305,7 +1432,7 @@ public void collectInitialPrefetchPositions(int adapterItemCount, } } - int direction = fromEnd + final int direction = fromEnd ? LayoutState.ITEM_DIRECTION_HEAD : LayoutState.ITEM_DIRECTION_TAIL; int targetPos = anchorPos; @@ -1319,21 +1446,6 @@ public void collectInitialPrefetchPositions(int adapterItemCount, } } - /** - * Gets the number of items to prefetch in - * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines - * how many inner items should be prefetched when this LayoutManager's RecyclerView - * is nested inside another RecyclerView. - * - * @return number of items to prefetch. - * @see #isItemPrefetchEnabled() - * @see #setInitialPrefetchItemCount(int) - * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) - */ - public int getInitialPrefetchItemCount() { - return mInitialPrefetchItemCount; - } - /** * Sets the number of items to prefetch in * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines @@ -1366,10 +1478,25 @@ public void setInitialPrefetchItemCount(int itemCount) { mInitialPrefetchItemCount = itemCount; } + /** + * Gets the number of items to prefetch in + * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines + * how many inner items should be prefetched when this LayoutManager's RecyclerView + * is nested inside another RecyclerView. + * + * @return number of items to prefetch. + * @see #isItemPrefetchEnabled() + * @see #setInitialPrefetchItemCount(int) + * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + */ + public int getInitialPrefetchItemCount() { + return mInitialPrefetchItemCount; + } + @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, - LayoutPrefetchRegistry layoutPrefetchRegistry) { + LayoutPrefetchRegistry layoutPrefetchRegistry) { int delta = (mOrientation == HORIZONTAL) ? dx : dy; if (getChildCount() == 0 || delta == 0) { // can't support this scroll, so don't bother prefetching @@ -1377,8 +1504,8 @@ public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State } ensureLayoutState(); - int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; - int absDelta = Math.abs(delta); + final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; + final int absDelta = Math.abs(delta); updateLayoutState(layoutDirection, absDelta, true, state); collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry); } @@ -1389,10 +1516,10 @@ int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state } ensureLayoutState(); mLayoutState.mRecycle = true; - int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; - int absDelta = Math.abs(delta); + final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; + final int absDelta = Math.abs(delta); updateLayoutState(layoutDirection, absDelta, true, state); - int consumed = mLayoutState.mScrollingOffset + final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); if (consumed < 0) { if (DEBUG) { @@ -1400,7 +1527,7 @@ int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state } return 0; } - int scrolled = absDelta > consumed ? layoutDirection * consumed : delta; + final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta; mOrientationHelper.offsetChildren(-scrolled); if (DEBUG) { Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled); @@ -1455,7 +1582,7 @@ private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int * #calculateExtraLayoutSpace}. */ private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset, - int noRecycleSpace) { + int noRecycleSpace) { if (scrollingOffset < 0) { if (DEBUG) { Log.d(TAG, "Called recycle from start with a negative value. This might happen" @@ -1464,8 +1591,8 @@ private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrolling return; } // ignore padding, ViewGroup may not clip children. - int limit = scrollingOffset - noRecycleSpace; - int childCount = getChildCount(); + final int limit = scrollingOffset - noRecycleSpace; + final int childCount = getChildCount(); if (mShouldReverseLayout) { for (int i = childCount - 1; i >= 0; i--) { View child = getChildAt(i); @@ -1504,8 +1631,8 @@ private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrolling * #calculateExtraLayoutSpace}. */ private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset, - int noRecycleSpace) { - int childCount = getChildCount(); + int noRecycleSpace) { + final int childCount = getChildCount(); if (scrollingOffset < 0) { if (DEBUG) { Log.d(TAG, "Called recycle from end with a negative value. This might happen" @@ -1513,7 +1640,7 @@ private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOf } return; } - int limit = mOrientationHelper.getEnd() - scrollingOffset + noRecycleSpace; + final int limit = mOrientationHelper.getEnd() - scrollingOffset + noRecycleSpace; if (mShouldReverseLayout) { for (int i = 0; i < childCount; i++) { View child = getChildAt(i); @@ -1573,9 +1700,9 @@ private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState la * @return Number of pixels that it added. Useful for scroll functions. */ int fill(RecyclerView.Recycler recycler, LayoutState layoutState, - RecyclerView.State state, boolean stopOnFocusable) { + RecyclerView.State state, boolean stopOnFocusable) { // max offset we should set is mFastScroll + available - int start = layoutState.mAvailable; + final int start = layoutState.mAvailable; if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { // TODO ugly bug fix. should not happen if (layoutState.mAvailable < 0) { @@ -1629,7 +1756,7 @@ int fill(RecyclerView.Recycler recycler, LayoutState layoutState, } void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, - LayoutState layoutState, LayoutChunkResult result) { + LayoutState layoutState, LayoutChunkResult result) { View view = layoutState.next(recycler); if (view == null) { if (DEBUG && layoutState.mScrapList == null) { @@ -1785,7 +1912,7 @@ private View getChildClosestToEnd() { * @return The first visible child closest to start of the layout from user's perspective. */ View findFirstVisibleChildClosestToStart(boolean completelyVisible, - boolean acceptPartiallyVisible) { + boolean acceptPartiallyVisible) { if (mShouldReverseLayout) { return findOneVisibleChild(getChildCount() - 1, -1, completelyVisible, acceptPartiallyVisible); @@ -1803,7 +1930,7 @@ View findFirstVisibleChildClosestToStart(boolean completelyVisible, * @return The first visible child closest to end of the layout from user's perspective. */ View findFirstVisibleChildClosestToEnd(boolean completelyVisible, - boolean acceptPartiallyVisible) { + boolean acceptPartiallyVisible) { if (mShouldReverseLayout) { return findOneVisibleChild(0, getChildCount(), completelyVisible, acceptPartiallyVisible); @@ -1828,14 +1955,14 @@ View findFirstVisibleChildClosestToEnd(boolean completelyVisible, *

  • An invalid child. * * - * @param layoutFromEnd True if the RV scrolls in the reverse direction, which is the same as - * (reverseLayout ^ stackFromEnd). + * @param layoutFromEnd True if the RV scrolls in the reverse direction, which is the same as + * (reverseLayout ^ stackFromEnd). * @param traverseChildrenInReverseOrder True if the children should be traversed in reverse * order (stackFromEnd). * @return A View that can be used an an anchor View. */ View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, - boolean layoutFromEnd, boolean traverseChildrenInReverseOrder) { + boolean layoutFromEnd, boolean traverseChildrenInReverseOrder) { ensureLayoutState(); // Determine which direction through the view children we are going iterate. @@ -1850,18 +1977,18 @@ View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state int itemCount = state.getItemCount(); - int boundsStart = mOrientationHelper.getStartAfterPadding(); - int boundsEnd = mOrientationHelper.getEndAfterPadding(); + final int boundsStart = mOrientationHelper.getStartAfterPadding(); + final int boundsEnd = mOrientationHelper.getEndAfterPadding(); View invalidMatch = null; View bestFirstFind = null; View bestSecondFind = null; for (int i = start; i != end; i += diff) { - View view = getChildAt(i); - int position = getPosition(view); - int childStart = mOrientationHelper.getDecoratedStart(view); - int childEnd = mOrientationHelper.getDecoratedEnd(view); + final View view = getChildAt(i); + final int position = getPosition(view); + final int childStart = mOrientationHelper.getDecoratedStart(view); + final int childEnd = mOrientationHelper.getDecoratedEnd(view); if (position >= 0 && position < itemCount) { if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) { if (invalidMatch == null) { @@ -1949,7 +2076,7 @@ private View findLastPartiallyOrCompletelyInvisibleChild() { * @see #findLastVisibleItemPosition() */ public int findFirstVisibleItemPosition() { - View child = findOneVisibleChild(0, getChildCount(), false, true); + final View child = findOneVisibleChild(0, getChildCount(), false, true); return child == null ? RecyclerView.NO_POSITION : getPosition(child); } @@ -1966,7 +2093,7 @@ public int findFirstVisibleItemPosition() { * @see #findLastCompletelyVisibleItemPosition() */ public int findFirstCompletelyVisibleItemPosition() { - View child = findOneVisibleChild(0, getChildCount(), true, false); + final View child = findOneVisibleChild(0, getChildCount(), true, false); return child == null ? RecyclerView.NO_POSITION : getPosition(child); } @@ -1989,7 +2116,7 @@ public int findFirstCompletelyVisibleItemPosition() { * @see #findFirstVisibleItemPosition() */ public int findLastVisibleItemPosition() { - View child = findOneVisibleChild(getChildCount() - 1, -1, false, true); + final View child = findOneVisibleChild(getChildCount() - 1, -1, false, true); return child == null ? RecyclerView.NO_POSITION : getPosition(child); } @@ -2006,7 +2133,7 @@ public int findLastVisibleItemPosition() { * @see #findFirstCompletelyVisibleItemPosition() */ public int findLastCompletelyVisibleItemPosition() { - View child = findOneVisibleChild(getChildCount() - 1, -1, true, false); + final View child = findOneVisibleChild(getChildCount() - 1, -1, true, false); return child == null ? RecyclerView.NO_POSITION : getPosition(child); } @@ -2015,9 +2142,9 @@ public int findLastCompletelyVisibleItemPosition() { // acceptable by this method, but could be returned // using #findOnePartiallyOrCompletelyInvisibleChild View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible, - boolean acceptPartiallyVisible) { + boolean acceptPartiallyVisible) { ensureLayoutState(); - @ViewBoundsCheck.ViewBounds int preferredBoundsFlag; + @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = 0; @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = 0; if (completelyVisible) { preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_GT_PVS | ViewBoundsCheck.FLAG_CVS_EQ_PVS @@ -2039,12 +2166,12 @@ View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible, View findOnePartiallyOrCompletelyInvisibleChild(int fromIndex, int toIndex) { ensureLayoutState(); - int next = toIndex > fromIndex ? 1 : (toIndex < fromIndex ? -1 : 0); + final int next = Integer.compare(toIndex, fromIndex); if (next == 0) { return getChildAt(fromIndex); } - @ViewBoundsCheck.ViewBounds int preferredBoundsFlag; - @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag; + @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = 0; + @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = 0; if (mOrientationHelper.getDecoratedStart(getChildAt(fromIndex)) < mOrientationHelper.getStartAfterPadding()) { preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVS | ViewBoundsCheck.FLAG_CVE_LT_PVE @@ -2067,18 +2194,18 @@ View findOnePartiallyOrCompletelyInvisibleChild(int fromIndex, int toIndex) { @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public View onFocusSearchFailed(View focused, int direction, - RecyclerView.Recycler recycler, RecyclerView.State state) { + RecyclerView.Recycler recycler, RecyclerView.State state) { resolveShouldLayoutReverse(); if (getChildCount() == 0) { return null; } - int layoutDir = convertFocusDirectionToLayoutDirection(direction); + final int layoutDir = convertFocusDirectionToLayoutDirection(direction); if (layoutDir == LayoutState.INVALID_LAYOUT) { return null; } ensureLayoutState(); - int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace()); + final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace()); updateLayoutState(layoutDir, maxScroll, false, state); mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN; mLayoutState.mRecycle = false; @@ -2088,7 +2215,7 @@ public View onFocusSearchFailed(View focused, int direction, // within RV's bounds, i.e. part of it is visible or it's completely invisible but still // touching RV's bounds. This will be the unfocusable candidate view to become visible onto // the screen if no focusable views are found in the given layout direction. - View nextCandidate; + final View nextCandidate; if (layoutDir == LayoutState.LAYOUT_START) { nextCandidate = findPartiallyOrCompletelyInvisibleChildClosestToStart(); } else { @@ -2096,7 +2223,7 @@ public View onFocusSearchFailed(View focused, int direction, } // nextFocus is meaningful only if it refers to a focusable child, in which case it // indicates the next view to gain focus. - View nextFocus; + final View nextFocus; if (layoutDir == LayoutState.LAYOUT_START) { nextFocus = getChildClosestToStart(); } else { @@ -2129,7 +2256,7 @@ private void logChildren() { * Used for debugging. * Validates that child views are laid out in correct order. This is important because rest of * the algorithm relies on this constraint. - *

    + * * In default layout, child 0 should be closest to screen position 0 and last child should be * closest to position WIDTH or HEIGHT. * In reverse layout, last child should be closes to screen position 0 and first child should @@ -2190,9 +2317,9 @@ public void prepareForDrop(@NonNull View view, @NonNull View target, int x, int assertNotInLayoutOrScroll("Cannot drop a view during a scroll or layout calculation"); ensureLayoutState(); resolveShouldLayoutReverse(); - int myPos = getPosition(view); - int targetPos = getPosition(target); - int dropDirection = myPos < targetPos ? LayoutState.ITEM_DIRECTION_TAIL + final int myPos = getPosition(view); + final int targetPos = getPosition(target); + final int dropDirection = myPos < targetPos ? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD; if (mShouldReverseLayout) { if (dropDirection == LayoutState.ITEM_DIRECTION_TAIL) { @@ -2280,21 +2407,21 @@ static class LayoutState { * The difference with {@link #mAvailable} is that, when recycling, distance laid out for * {@link #mExtraFillSpace} is not considered to avoid recycling visible children. */ - int mExtraFillSpace; + int mExtraFillSpace = 0; /** * Contains the {@link #calculateExtraLayoutSpace(RecyclerView.State, int[])} extra layout * space} that should be excluded for recycling when cleaning up the tail of the list during * a smooth scroll. */ - int mNoRecycleSpace; + int mNoRecycleSpace = 0; /** * Equal to {@link RecyclerView.State#isPreLayout()}. When consuming scrap, if this value * is set to true, we skip removed views since they should not be laid out in post layout * step. */ - boolean mIsPreLayout; + boolean mIsPreLayout = false; /** * The most recent {@link #scrollBy(int, RecyclerView.Recycler, RecyclerView.State)} @@ -2306,7 +2433,7 @@ static class LayoutState { * When LLM needs to layout particular views, it sets this list in which case, LayoutState * will only return views from this list and return null if it cannot find an item. */ - List mScrapList; + List mScrapList = null; /** * Used when there is no limit in how many views can be laid out. @@ -2330,7 +2457,7 @@ View next(RecyclerView.Recycler recycler) { if (mScrapList != null) { return nextViewFromScrapList(); } - View view = recycler.getViewForPosition(mCurrentPosition); + final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; } @@ -2343,10 +2470,10 @@ View next(RecyclerView.Recycler recycler) { * @return View if an item in the current position or direction exists if not null. */ private View nextViewFromScrapList() { - int size = mScrapList.size(); + final int size = mScrapList.size(); for (int i = 0; i < size; i++) { - View view = mScrapList.get(i).itemView; - RecyclerView.LayoutParams lp = + final View view = mScrapList.get(i).itemView; + final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams(); if (lp.isItemRemoved()) { continue; @@ -2364,7 +2491,7 @@ public void assignPositionFromScrapList() { } public void assignPositionFromScrapList(View ignore) { - View closest = nextViewInLimitedList(ignore); + final View closest = nextViewInLimitedList(ignore); if (closest == null) { mCurrentPosition = RecyclerView.NO_POSITION; } else { @@ -2382,12 +2509,12 @@ public View nextViewInLimitedList(View ignore) { } for (int i = 0; i < size; i++) { View view = mScrapList.get(i).itemView; - RecyclerView.LayoutParams lp = + final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams(); if (view == ignore || lp.isItemRemoved()) { continue; } - int distance = (lp.getViewLayoutPosition() - mCurrentPosition) + final int distance = (lp.getViewLayoutPosition() - mCurrentPosition) * mItemDirection; if (distance < 0) { continue; // item is not in current direction @@ -2457,45 +2584,45 @@ boolean isViewValidAsAnchor(View child, RecyclerView.State state) { } public void assignFromViewAndKeepVisibleRect(View child, int position) { - int spaceChange = mOrientationHelper.getTotalSpaceChange(); + final int spaceChange = mOrientationHelper.getTotalSpaceChange(); if (spaceChange >= 0) { assignFromView(child, position); return; } mPosition = position; if (mLayoutFromEnd) { - int prevLayoutEnd = mOrientationHelper.getEndAfterPadding() - spaceChange; - int childEnd = mOrientationHelper.getDecoratedEnd(child); - int previousEndMargin = prevLayoutEnd - childEnd; + final int prevLayoutEnd = mOrientationHelper.getEndAfterPadding() - spaceChange; + final int childEnd = mOrientationHelper.getDecoratedEnd(child); + final int previousEndMargin = prevLayoutEnd - childEnd; mCoordinate = mOrientationHelper.getEndAfterPadding() - previousEndMargin; // ensure we did not push child's top out of bounds because of this if (previousEndMargin > 0) { // we have room to shift bottom if necessary - int childSize = mOrientationHelper.getDecoratedMeasurement(child); - int estimatedChildStart = mCoordinate - childSize; - int layoutStart = mOrientationHelper.getStartAfterPadding(); - int previousStartMargin = mOrientationHelper.getDecoratedStart(child) + final int childSize = mOrientationHelper.getDecoratedMeasurement(child); + final int estimatedChildStart = mCoordinate - childSize; + final int layoutStart = mOrientationHelper.getStartAfterPadding(); + final int previousStartMargin = mOrientationHelper.getDecoratedStart(child) - layoutStart; - int startReference = layoutStart + Math.min(previousStartMargin, 0); - int startMargin = estimatedChildStart - startReference; + final int startReference = layoutStart + Math.min(previousStartMargin, 0); + final int startMargin = estimatedChildStart - startReference; if (startMargin < 0) { // offset to make top visible but not too much mCoordinate += Math.min(previousEndMargin, -startMargin); } } } else { - int childStart = mOrientationHelper.getDecoratedStart(child); - int startMargin = childStart - mOrientationHelper.getStartAfterPadding(); + final int childStart = mOrientationHelper.getDecoratedStart(child); + final int startMargin = childStart - mOrientationHelper.getStartAfterPadding(); mCoordinate = childStart; if (startMargin > 0) { // we have room to fix end as well - int estimatedEnd = childStart + final int estimatedEnd = childStart + mOrientationHelper.getDecoratedMeasurement(child); - int previousLayoutEnd = mOrientationHelper.getEndAfterPadding() + final int previousLayoutEnd = mOrientationHelper.getEndAfterPadding() - spaceChange; - int previousEndMargin = previousLayoutEnd + final int previousEndMargin = previousLayoutEnd - mOrientationHelper.getDecoratedEnd(child); - int endReference = mOrientationHelper.getEndAfterPadding() + final int endReference = mOrientationHelper.getEndAfterPadding() - Math.min(0, previousEndMargin); - int endMargin = endReference - estimatedEnd; + final int endMargin = endReference - estimatedEnd; if (endMargin < 0) { mCoordinate -= Math.min(startMargin, -endMargin); } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/LinearSmoothScroller.java b/viewpager2/src/main/java/androidx/recyclerview/widget/LinearSmoothScroller.java index 9ba2d034c..b4ba75fcd 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/LinearSmoothScroller.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/LinearSmoothScroller.java @@ -36,6 +36,12 @@ */ public class LinearSmoothScroller extends RecyclerView.SmoothScroller { + private static final boolean DEBUG = false; + + private static final float MILLISECONDS_PER_INCH = 25f; + + private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000; + /** * Align child view's left or top with parent view's left or top * @@ -44,6 +50,7 @@ public class LinearSmoothScroller extends RecyclerView.SmoothScroller { * @see #calculateDyToMakeVisible(android.view.View, int) */ public static final int SNAP_TO_START = -1; + /** * Align child view's right or bottom with parent view's right or bottom * @@ -52,6 +59,7 @@ public class LinearSmoothScroller extends RecyclerView.SmoothScroller { * @see #calculateDyToMakeVisible(android.view.View, int) */ public static final int SNAP_TO_END = 1; + /** *

    Decides if the child should be snapped from start or end, depending on where it * currently is in relation to its parent.

    @@ -63,9 +71,7 @@ public class LinearSmoothScroller extends RecyclerView.SmoothScroller { * @see #calculateDyToMakeVisible(android.view.View, int) */ public static final int SNAP_TO_ANY = 0; - private static final boolean DEBUG = false; - private static final float MILLISECONDS_PER_INCH = 25f; - private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000; + // Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target // view is not laid out until interim target position is reached, we can detect the case before // scrolling slows down and reschedule another interim target scroll @@ -74,14 +80,17 @@ public class LinearSmoothScroller extends RecyclerView.SmoothScroller { protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator(); protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator(); - private final DisplayMetrics mDisplayMetrics; + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly protected PointF mTargetVector; + + private final DisplayMetrics mDisplayMetrics; + private boolean mHasCalculatedMillisPerPixel = false; + private float mMillisPerPixel; + // Temporary variables to keep track of the interim scroll target. These values do not // point to a real item position, rather point to an estimated location pixels. - protected int mInterimTargetDx, mInterimTargetDy; - private boolean mHasCalculatedMillisPerPixel; - private float mMillisPerPixel; + protected int mInterimTargetDx = 0, mInterimTargetDy = 0; @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public LinearSmoothScroller(Context context) { @@ -102,10 +111,10 @@ protected void onStart() { @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { - int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference()); - int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference()); - int distance = (int) Math.sqrt(dx * dx + dy * dy); - int time = calculateTimeForDeceleration(distance); + final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference()); + final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference()); + final int distance = (int) Math.sqrt(dx * dx + dy * dy); + final int time = calculateTimeForDeceleration(distance); if (time > 0) { action.update(-dx, -dy, time, mDecelerateInterpolator); } @@ -124,6 +133,7 @@ protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action stop(); return; } + //noinspection PointlessBooleanExpression if (DEBUG && mTargetVector != null && (mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0)) { throw new IllegalStateException("Scroll happened in the opposite direction" @@ -184,7 +194,7 @@ protected int calculateTimeForDeceleration(int dx) { // area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x // which gives 0.100028 when x = .3356 // this is why we divide linear scrolling time with .3356 - return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356); + return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356); } /** @@ -240,7 +250,7 @@ protected void updateActionForInterimTarget(Action action) { // find an interim target position PointF scrollVector = computeScrollVectorForPosition(getTargetPosition()); if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) { - int target = getTargetPosition(); + final int target = getTargetPosition(); action.jumpTo(target); stop(); return; @@ -250,7 +260,7 @@ protected void updateActionForInterimTarget(Action action) { mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x); mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y); - int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX); + final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX); // To avoid UI hiccups, trigger a smooth scroll to a distance little further than the // interim target. Since we track the distance travelled in onSeekTargetStep callback, it // won't actually scroll more than what we need. @@ -260,7 +270,7 @@ protected void updateActionForInterimTarget(Action action) { } private int clampApplyScroll(int tmpDt, int dt) { - int before = tmpDt; + final int before = tmpDt; tmpDt -= dt; if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset return 0; @@ -280,11 +290,11 @@ public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd case SNAP_TO_END: return boxEnd - viewEnd; case SNAP_TO_ANY: - int dtStart = boxStart - viewStart; + final int dtStart = boxStart - viewStart; if (dtStart > 0) { return dtStart; } - int dtEnd = boxEnd - viewEnd; + final int dtEnd = boxEnd - viewEnd; if (dtEnd < 0) { return dtEnd; } @@ -309,16 +319,16 @@ public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd */ @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public int calculateDyToMakeVisible(View view, int snapPreference) { - RecyclerView.LayoutManager layoutManager = getLayoutManager(); + final RecyclerView.LayoutManager layoutManager = getLayoutManager(); if (layoutManager == null || !layoutManager.canScrollVertically()) { return 0; } - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); - int top = layoutManager.getDecoratedTop(view) - params.topMargin; - int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin; - int start = layoutManager.getPaddingTop(); - int end = layoutManager.getHeight() - layoutManager.getPaddingBottom(); + final int top = layoutManager.getDecoratedTop(view) - params.topMargin; + final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin; + final int start = layoutManager.getPaddingTop(); + final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom(); return calculateDtToFit(top, bottom, start, end, snapPreference); } @@ -335,16 +345,16 @@ public int calculateDyToMakeVisible(View view, int snapPreference) { */ @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public int calculateDxToMakeVisible(View view, int snapPreference) { - RecyclerView.LayoutManager layoutManager = getLayoutManager(); + final RecyclerView.LayoutManager layoutManager = getLayoutManager(); if (layoutManager == null || !layoutManager.canScrollHorizontally()) { return 0; } - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); - int left = layoutManager.getDecoratedLeft(view) - params.leftMargin; - int right = layoutManager.getDecoratedRight(view) + params.rightMargin; - int start = layoutManager.getPaddingLeft(); - int end = layoutManager.getWidth() - layoutManager.getPaddingRight(); + final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin; + final int right = layoutManager.getDecoratedRight(view) + params.rightMargin; + final int start = layoutManager.getPaddingLeft(); + final int end = layoutManager.getWidth() - layoutManager.getPaddingRight(); return calculateDtToFit(left, right, start, end, snapPreference); } -} \ No newline at end of file +} diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/LinearSnapHelper.java b/viewpager2/src/main/java/androidx/recyclerview/widget/LinearSnapHelper.java index f77f766a7..e04cc7347 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/LinearSnapHelper.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/LinearSnapHelper.java @@ -61,23 +61,23 @@ public int[] calculateDistanceToFinalSnap( } @Override - public int findTargetSnapPosition(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX, - int velocityY) { + public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, + int velocityY) { if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { return RecyclerView.NO_POSITION; } - int itemCount = layoutManager.getItemCount(); + final int itemCount = layoutManager.getItemCount(); if (itemCount == 0) { return RecyclerView.NO_POSITION; } - View currentView = findSnapView(layoutManager); + final View currentView = findSnapView(layoutManager); if (currentView == null) { return RecyclerView.NO_POSITION; } - int currentPosition = layoutManager.getPosition(currentView); + final int currentPosition = layoutManager.getPosition(currentView); if (currentPosition == RecyclerView.NO_POSITION) { return RecyclerView.NO_POSITION; } @@ -139,9 +139,9 @@ public View findSnapView(RecyclerView.LayoutManager layoutManager) { } private int distanceToCenter(@NonNull View targetView, OrientationHelper helper) { - int childCenter = helper.getDecoratedStart(targetView) + final int childCenter = helper.getDecoratedStart(targetView) + (helper.getDecoratedMeasurement(targetView) / 2); - int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; + final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; return childCenter - containerCenter; } @@ -153,10 +153,11 @@ private int distanceToCenter(@NonNull View targetView, OrientationHelper helper) * @param helper The {@link OrientationHelper} that is created from the LayoutManager. * @param velocityX The velocity on the x axis. * @param velocityY The velocity on the y axis. + * * @return The diff between the target scroll position and the current position. */ private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager, - OrientationHelper helper, int velocityX, int velocityY) { + OrientationHelper helper, int velocityX, int velocityY) { int[] distances = calculateScrollDistance(velocityX, velocityY); float distancePerChild = computeDistancePerChild(layoutManager, helper); if (distancePerChild <= 0) { @@ -172,23 +173,24 @@ private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutMa * * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached * {@link RecyclerView}. - * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}. + * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}. + * * @return the child view that is currently closest to the center of this parent. */ @Nullable private View findCenterView(RecyclerView.LayoutManager layoutManager, - OrientationHelper helper) { + OrientationHelper helper) { int childCount = layoutManager.getChildCount(); if (childCount == 0) { return null; } View closestChild = null; - int center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; + final int center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; int absClosest = Integer.MAX_VALUE; for (int i = 0; i < childCount; i++) { - View child = layoutManager.getChildAt(i); + final View child = layoutManager.getChildAt(i); int childCenter = helper.getDecoratedStart(child) + (helper.getDecoratedMeasurement(child) / 2); int absDistance = Math.abs(childCenter - center); @@ -211,11 +213,12 @@ private View findCenterView(RecyclerView.LayoutManager layoutManager, * {@link RecyclerView}. * @param helper The relevant {@link OrientationHelper} for the attached * {@link RecyclerView.LayoutManager}. + * * @return A float value that is the average number of pixels needed to scroll by one view in * the relevant direction. */ private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager, - OrientationHelper helper) { + OrientationHelper helper) { View minPosView = null; View maxPosView = null; int minPos = Integer.MAX_VALUE; @@ -227,7 +230,7 @@ private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager, for (int i = 0; i < childCount; i++) { View child = layoutManager.getChildAt(i); - int pos = layoutManager.getPosition(child); + final int pos = layoutManager.getPosition(child); if (pos == RecyclerView.NO_POSITION) { continue; } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/ListAdapter.java b/viewpager2/src/main/java/androidx/recyclerview/widget/ListAdapter.java index 43d15d04b..57341d356 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/ListAdapter.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/ListAdapter.java @@ -83,19 +83,19 @@ * } * } * }
  • - *

    + * * Advanced users that wish for more control over adapter behavior, or to provide a specific base * class should refer to {@link AsyncListDiffer}, which provides custom mapping from diff events * to adapter positions. * - * @param Type of the Lists this Adapter will receive. + * @param Type of the Lists this Adapter will receive. * @param A class that extends ViewHolder that will be used by the adapter. */ public abstract class ListAdapter extends RecyclerView.Adapter { final AsyncListDiffer mDiffer; private final AsyncListDiffer.ListListener mListener = - this::onCurrentListChanged; + ListAdapter.this::onCurrentListChanged; @SuppressWarnings("unused") protected ListAdapter(@NonNull DiffUtil.ItemCallback diffCallback) { @@ -132,11 +132,11 @@ public void submitList(@Nullable List list) { * may not be executed. If List B is submitted immediately after List A, and is * committed directly, the callback associated with List A will not be run. * - * @param list The new list to be displayed. + * @param list The new list to be displayed. * @param commitCallback Optional runnable that is executed when the List is committed, if * it is committed. */ - public void submitList(@Nullable List list, @Nullable Runnable commitCallback) { + public void submitList(@Nullable List list, @Nullable final Runnable commitCallback) { mDiffer.submitList(list, commitCallback); } @@ -159,6 +159,7 @@ public int getItemCount() { * {@link #submitList(List)}. * * @return The list currently being displayed. + * * @see #onCurrentListChanged(List, List) */ @NonNull @@ -173,8 +174,9 @@ public List getCurrentList() { * submitted, the current List is represented as an empty List. * * @param previousList List that was displayed previously. - * @param currentList new List being displayed, will be empty if {@code null} was passed to - * {@link #submitList(List)}. + * @param currentList new List being displayed, will be empty if {@code null} was passed to + * {@link #submitList(List)}. + * * @see #getCurrentList() */ public void onCurrentListChanged(@NonNull List previousList, @NonNull List currentList) { diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/MessageThreadUtil.java b/viewpager2/src/main/java/androidx/recyclerview/widget/MessageThreadUtil.java index 81622a07e..812c6b415 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/MessageThreadUtil.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/MessageThreadUtil.java @@ -20,43 +20,20 @@ import android.os.Looper; import android.util.Log; -import androidx.annotation.NonNull; - import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; class MessageThreadUtil implements ThreadUtil { @Override - public MainThreadCallback getMainThreadProxy(MainThreadCallback callback) { + public MainThreadCallback getMainThreadProxy(final MainThreadCallback callback) { return new MainThreadCallback() { + final MessageQueue mQueue = new MessageQueue(); + final private Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + static final int UPDATE_ITEM_COUNT = 1; static final int ADD_TILE = 2; static final int REMOVE_TILE = 3; - final MessageQueue mQueue = new MessageQueue(); - final private Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); - private final Runnable mMainThreadRunnable = () -> { - SyncQueueItem msg = mQueue.next(); - while (msg != null) { - switch (msg.what) { - case UPDATE_ITEM_COUNT: - callback.updateItemCount(msg.arg1, msg.arg2); - break; - case ADD_TILE: - @SuppressWarnings("unchecked") - TileList.Tile tile = (TileList.Tile) msg.data; - callback.addTile(msg.arg1, tile); - break; - case REMOVE_TILE: - callback.removeTile(msg.arg1, msg.arg2); - break; - default: - Log.e("ThreadUtil", "Unsupported message, what=" + msg.what); - } - msg = mQueue.next(); - } - }; @Override public void updateItemCount(int generation, int itemCount) { @@ -64,7 +41,7 @@ public void updateItemCount(int generation, int itemCount) { } @Override - public void addTile(int generation, @NonNull TileList.Tile tile) { + public void addTile(int generation, TileList.Tile tile) { sendMessage(SyncQueueItem.obtainMessage(ADD_TILE, generation, tile)); } @@ -77,51 +54,43 @@ private void sendMessage(SyncQueueItem msg) { mQueue.sendMessage(msg); mMainThreadHandler.post(mMainThreadRunnable); } - }; - } - /* AsyncTask */ - @Override - public BackgroundCallback getBackgroundProxy(BackgroundCallback callback) { - return new BackgroundCallback() { - static final int REFRESH = 1; - static final int UPDATE_RANGE = 2; - static final int LOAD_TILE = 3; - static final int RECYCLE_TILE = 4; - final MessageQueue mQueue = new MessageQueue(); - final AtomicBoolean mBackgroundRunning = new AtomicBoolean(false); - private final Executor mExecutor = Executors.newSingleThreadExecutor(); - private final Runnable mBackgroundRunnable = () -> { - while (true) { - SyncQueueItem msg = mQueue.next(); - if (msg == null) { - break; - } + private Runnable mMainThreadRunnable = () -> { + SyncQueueItem msg = mQueue.next(); + while (msg != null) { switch (msg.what) { - case REFRESH: - mQueue.removeMessages(REFRESH); - callback.refresh(msg.arg1); - break; - case UPDATE_RANGE: - mQueue.removeMessages(UPDATE_RANGE); - mQueue.removeMessages(LOAD_TILE); - callback.updateRange( - msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5); - break; - case LOAD_TILE: - callback.loadTile(msg.arg1, msg.arg2); + case UPDATE_ITEM_COUNT: + callback.updateItemCount(msg.arg1, msg.arg2); break; - case RECYCLE_TILE: + case ADD_TILE: @SuppressWarnings("unchecked") TileList.Tile tile = (TileList.Tile) msg.data; - callback.recycleTile(tile); + callback.addTile(msg.arg1, tile); + break; + case REMOVE_TILE: + callback.removeTile(msg.arg1, msg.arg2); break; default: Log.e("ThreadUtil", "Unsupported message, what=" + msg.what); } + msg = mQueue.next(); } - mBackgroundRunning.set(false); }; + }; + } + + @SuppressWarnings("deprecation") /* AsyncTask */ + @Override + public BackgroundCallback getBackgroundProxy(final BackgroundCallback callback) { + return new BackgroundCallback() { + final MessageQueue mQueue = new MessageQueue(); + private final Executor mExecutor = android.os.AsyncTask.THREAD_POOL_EXECUTOR; + AtomicBoolean mBackgroundRunning = new AtomicBoolean(false); + + static final int REFRESH = 1; + static final int UPDATE_RANGE = 2; + static final int LOAD_TILE = 3; + static final int RECYCLE_TILE = 4; @Override public void refresh(int generation) { @@ -141,7 +110,7 @@ public void loadTile(int position, int scrollHint) { } @Override - public void recycleTile(@NonNull TileList.Tile tile) { + public void recycleTile(TileList.Tile tile) { sendMessage(SyncQueueItem.obtainMessage(RECYCLE_TILE, 0, tile)); } @@ -160,6 +129,38 @@ private void maybeExecuteBackgroundRunnable() { mExecutor.execute(mBackgroundRunnable); } } + + private Runnable mBackgroundRunnable = () -> { + while (true) { + SyncQueueItem msg = mQueue.next(); + if (msg == null) { + break; + } + switch (msg.what) { + case REFRESH: + mQueue.removeMessages(REFRESH); + callback.refresh(msg.arg1); + break; + case UPDATE_RANGE: + mQueue.removeMessages(UPDATE_RANGE); + mQueue.removeMessages(LOAD_TILE); + callback.updateRange( + msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5); + break; + case LOAD_TILE: + callback.loadTile(msg.arg1, msg.arg2); + break; + case RECYCLE_TILE: + @SuppressWarnings("unchecked") + TileList.Tile tile = (TileList.Tile) msg.data; + callback.recycleTile(tile); + break; + default: + Log.e("ThreadUtil", "Unsupported message, what=" + msg.what); + } + } + mBackgroundRunning.set(false); + }; }; } @@ -169,8 +170,9 @@ private void maybeExecuteBackgroundRunnable() { */ static class SyncQueueItem { - private static final Object sPoolLock = new Object(); private static SyncQueueItem sPool; + private static final Object sPoolLock = new Object(); + SyncQueueItem next; public int what; public int arg1; public int arg2; @@ -178,12 +180,23 @@ static class SyncQueueItem { public int arg4; public int arg5; public Object data; - SyncQueueItem next; + + void recycle() { + next = null; + what = arg1 = arg2 = arg3 = arg4 = arg5 = 0; + data = null; + synchronized (sPoolLock) { + if (sPool != null) { + next = sPool; + } + sPool = this; + } + } static SyncQueueItem obtainMessage(int what, int arg1, int arg2, int arg3, int arg4, int arg5, Object data) { synchronized (sPoolLock) { - SyncQueueItem item; + final SyncQueueItem item; if (sPool == null) { item = new SyncQueueItem(); } else { @@ -209,31 +222,19 @@ static SyncQueueItem obtainMessage(int what, int arg1, int arg2) { static SyncQueueItem obtainMessage(int what, int arg1, Object data) { return obtainMessage(what, arg1, 0, 0, 0, 0, data); } - - void recycle() { - next = null; - what = arg1 = arg2 = arg3 = arg4 = arg5 = 0; - data = null; - synchronized (sPoolLock) { - if (sPool != null) { - next = sPool; - } - sPool = this; - } - } } static class MessageQueue { - private final Object mLock = new Object(); private SyncQueueItem mRoot; + private final Object mLock = new Object(); SyncQueueItem next() { synchronized (mLock) { if (mRoot == null) { return null; } - SyncQueueItem next = mRoot; + final SyncQueueItem next = mRoot; mRoot = mRoot.next; return next; } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/NestedAdapterWrapper.java b/viewpager2/src/main/java/androidx/recyclerview/widget/NestedAdapterWrapper.java index ed1085bc9..a6b9a300e 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/NestedAdapterWrapper.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/NestedAdapterWrapper.java @@ -30,13 +30,13 @@ * Wrapper for each adapter in {@link ConcatAdapter}. */ class NestedAdapterWrapper { - public final Adapter adapter; - @SuppressWarnings("WeakerAccess") - final Callback mCallback; @NonNull private final ViewTypeStorage.ViewTypeLookup mViewTypeLookup; @NonNull private final StableIdStorage.StableIdLookup mStableIdLookup; + public final Adapter adapter; + @SuppressWarnings("WeakerAccess") + final Callback mCallback; // we cache this value so that we can know the previous size when change happens // this is also important as getting real size while an adapter is dispatching possibly a // a chain of events might create inconsistencies (as it happens in DiffUtil). @@ -44,7 +44,7 @@ class NestedAdapterWrapper { @SuppressWarnings("WeakerAccess") int mCachedItemCount; - private final RecyclerView.AdapterDataObserver mAdapterObserver = + private RecyclerView.AdapterDataObserver mAdapterObserver = new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { @@ -64,7 +64,7 @@ public void onItemRangeChanged(int positionStart, int itemCount) { @Override public void onItemRangeChanged(int positionStart, int itemCount, - @Nullable Object payload) { + @Nullable Object payload) { mCallback.onItemRangeChanged( NestedAdapterWrapper.this, positionStart, @@ -121,9 +121,9 @@ public void onStateRestorationPolicyChanged() { NestedAdapterWrapper( Adapter adapter, - Callback callback, + final Callback callback, ViewTypeStorage viewTypeStorage, - @NonNull StableIdStorage.StableIdLookup stableIdLookup) { + StableIdStorage.StableIdLookup stableIdLookup) { this.adapter = adapter; mCallback = callback; mViewTypeLookup = viewTypeStorage.createViewTypeWrapper(this); diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/OpReorderer.java b/viewpager2/src/main/java/androidx/recyclerview/widget/OpReorderer.java index b50de76b1..722960c82 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/OpReorderer.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/OpReorderer.java @@ -36,8 +36,8 @@ void reorderOps(List ops) { } private void swapMoveOp(List list, int badMove, int next) { - AdapterHelper.UpdateOp moveOp = list.get(badMove); - AdapterHelper.UpdateOp nextOp = list.get(next); + final AdapterHelper.UpdateOp moveOp = list.get(badMove); + final AdapterHelper.UpdateOp nextOp = list.get(next); switch (nextOp.cmd) { case AdapterHelper.UpdateOp.REMOVE: swapMoveRemove(list, badMove, moveOp, next, nextOp); @@ -52,11 +52,11 @@ private void swapMoveOp(List list, int badMove, int next } void swapMoveRemove(List list, int movePos, AdapterHelper.UpdateOp moveOp, - int removePos, AdapterHelper.UpdateOp removeOp) { + int removePos, AdapterHelper.UpdateOp removeOp) { AdapterHelper.UpdateOp extraRm = null; // check if move is nulled out by remove boolean revertedMove = false; - boolean moveIsBackwards; + final boolean moveIsBackwards; if (moveOp.positionStart < moveOp.itemCount) { moveIsBackwards = false; @@ -92,7 +92,7 @@ void swapMoveRemove(List list, int movePos, AdapterHelpe if (moveOp.positionStart <= removeOp.positionStart) { removeOp.positionStart++; } else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) { - int remaining = removeOp.positionStart + removeOp.itemCount + final int remaining = removeOp.positionStart + removeOp.itemCount - moveOp.positionStart; extraRm = mCallback.obtainUpdateOp(AdapterHelper.UpdateOp.REMOVE, moveOp.positionStart + 1, remaining, null); removeOp.itemCount = moveOp.positionStart - removeOp.positionStart; @@ -151,7 +151,7 @@ void swapMoveRemove(List list, int movePos, AdapterHelpe } private void swapMoveAdd(List list, int move, AdapterHelper.UpdateOp moveOp, int add, - AdapterHelper.UpdateOp addOp) { + AdapterHelper.UpdateOp addOp) { int offset = 0; // going in reverse, first revert the effect of add if (moveOp.itemCount < addOp.positionStart) { @@ -172,7 +172,7 @@ private void swapMoveAdd(List list, int move, AdapterHel } void swapMoveUpdate(List list, int move, AdapterHelper.UpdateOp moveOp, int update, - AdapterHelper.UpdateOp updateOp) { + AdapterHelper.UpdateOp updateOp) { AdapterHelper.UpdateOp extraUp1 = null; AdapterHelper.UpdateOp extraUp2 = null; // going in reverse, first revert the effect of add @@ -187,7 +187,7 @@ void swapMoveUpdate(List list, int move, AdapterHelper.U if (moveOp.positionStart <= updateOp.positionStart) { updateOp.positionStart++; } else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) { - int remaining = updateOp.positionStart + updateOp.itemCount + final int remaining = updateOp.positionStart + updateOp.itemCount - moveOp.positionStart; extraUp2 = mCallback.obtainUpdateOp( AdapterHelper.UpdateOp.UPDATE, moveOp.positionStart + 1, remaining, @@ -212,7 +212,7 @@ void swapMoveUpdate(List list, int move, AdapterHelper.U private int getLastMoveOutOfOrder(List list) { boolean foundNonMove = false; for (int i = list.size() - 1; i >= 0; i--) { - AdapterHelper.UpdateOp op1 = list.get(i); + final AdapterHelper.UpdateOp op1 = list.get(i); if (op1.cmd == AdapterHelper.UpdateOp.MOVE) { if (foundNonMove) { return i; diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/OrientationHelper.java b/viewpager2/src/main/java/androidx/recyclerview/widget/OrientationHelper.java index 5bc9a8c57..f94e0dd16 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/OrientationHelper.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/OrientationHelper.java @@ -31,17 +31,204 @@ */ public abstract class OrientationHelper { - public static final int HORIZONTAL = RecyclerView.HORIZONTAL; - public static final int VERTICAL = RecyclerView.VERTICAL; private static final int INVALID_SIZE = Integer.MIN_VALUE; + protected final RecyclerView.LayoutManager mLayoutManager; - final Rect mTmpRect = new Rect(); + + public static final int HORIZONTAL = RecyclerView.HORIZONTAL; + + public static final int VERTICAL = RecyclerView.VERTICAL; + private int mLastTotalSpace = INVALID_SIZE; + final Rect mTmpRect = new Rect(); + private OrientationHelper(RecyclerView.LayoutManager layoutManager) { mLayoutManager = layoutManager; } + /** + * Returns the {@link RecyclerView.LayoutManager LayoutManager} that + * is associated with this OrientationHelper. + */ + public RecyclerView.LayoutManager getLayoutManager() { + return mLayoutManager; + } + + /** + * Call this method after onLayout method is complete if state is NOT pre-layout. + * This method records information like layout bounds that might be useful in the next layout + * calculations. + */ + public void onLayoutComplete() { + mLastTotalSpace = getTotalSpace(); + } + + /** + * Returns the layout space change between the previous layout pass and current layout pass. + *

    + * Make sure you call {@link #onLayoutComplete()} at the end of your LayoutManager's + * {@link RecyclerView.LayoutManager#onLayoutChildren(RecyclerView.Recycler, + * RecyclerView.State)} method. + * + * @return The difference between the current total space and previous layout's total space. + * @see #onLayoutComplete() + */ + public int getTotalSpaceChange() { + return INVALID_SIZE == mLastTotalSpace ? 0 : getTotalSpace() - mLastTotalSpace; + } + + /** + * Returns the start of the view including its decoration and margin. + *

    + * For example, for the horizontal helper, if a View's left is at pixel 20, has 2px left + * decoration and 3px left margin, returned value will be 15px. + * + * @param view The view element to check + * @return The first pixel of the element + * @see #getDecoratedEnd(android.view.View) + */ + public abstract int getDecoratedStart(View view); + + /** + * Returns the end of the view including its decoration and margin. + *

    + * For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right + * decoration and 3px right margin, returned value will be 205. + * + * @param view The view element to check + * @return The last pixel of the element + * @see #getDecoratedStart(android.view.View) + */ + public abstract int getDecoratedEnd(View view); + + /** + * Returns the end of the View after its matrix transformations are applied to its layout + * position. + *

    + * This method is useful when trying to detect the visible edge of a View. + *

    + * It includes the decorations but does not include the margins. + * + * @param view The view whose transformed end will be returned + * @return The end of the View after its decor insets and transformation matrix is applied to + * its position + * + * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect) + */ + public abstract int getTransformedEndWithDecoration(View view); + + /** + * Returns the start of the View after its matrix transformations are applied to its layout + * position. + *

    + * This method is useful when trying to detect the visible edge of a View. + *

    + * It includes the decorations but does not include the margins. + * + * @param view The view whose transformed start will be returned + * @return The start of the View after its decor insets and transformation matrix is applied to + * its position + * + * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect) + */ + public abstract int getTransformedStartWithDecoration(View view); + + /** + * Returns the space occupied by this View in the current orientation including decorations and + * margins. + * + * @param view The view element to check + * @return Total space occupied by this view + * @see #getDecoratedMeasurementInOther(View) + */ + public abstract int getDecoratedMeasurement(View view); + + /** + * Returns the space occupied by this View in the perpendicular orientation including + * decorations and margins. + * + * @param view The view element to check + * @return Total space occupied by this view in the perpendicular orientation to current one + * @see #getDecoratedMeasurement(View) + */ + public abstract int getDecoratedMeasurementInOther(View view); + + /** + * Returns the start position of the layout after the start padding is added. + * + * @return The very first pixel we can draw. + */ + public abstract int getStartAfterPadding(); + + /** + * Returns the end position of the layout after the end padding is removed. + * + * @return The end boundary for this layout. + */ + public abstract int getEndAfterPadding(); + + /** + * Returns the end position of the layout without taking padding into account. + * + * @return The end boundary for this layout without considering padding. + */ + public abstract int getEnd(); + + /** + * Offsets all children's positions by the given amount. + * + * @param amount Value to add to each child's layout parameters + */ + public abstract void offsetChildren(int amount); + + /** + * Returns the total space to layout. This number is the difference between + * {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}. + * + * @return Total space to layout children + */ + public abstract int getTotalSpace(); + + /** + * Offsets the child in this orientation. + * + * @param view View to offset + * @param offset offset amount + */ + public abstract void offsetChild(View view, int offset); + + /** + * Returns the padding at the end of the layout. For horizontal helper, this is the right + * padding and for vertical helper, this is the bottom padding. This method does not check + * whether the layout is RTL or not. + * + * @return The padding at the end of the layout. + */ + public abstract int getEndPadding(); + + /** + * Returns the MeasureSpec mode for the current orientation from the LayoutManager. + * + * @return The current measure spec mode. + * + * @see View.MeasureSpec + * @see RecyclerView.LayoutManager#getWidthMode() + * @see RecyclerView.LayoutManager#getHeightMode() + */ + public abstract int getMode(); + + /** + * Returns the MeasureSpec mode for the perpendicular orientation from the LayoutManager. + * + * @return The current measure spec mode. + * + * @see View.MeasureSpec + * @see RecyclerView.LayoutManager#getWidthMode() + * @see RecyclerView.LayoutManager#getHeightMode() + */ + public abstract int getModeInOther(); + /** * Creates an OrientationHelper for the given LayoutManager and orientation. * @@ -91,7 +278,7 @@ public int getStartAfterPadding() { @Override public int getDecoratedMeasurement(View view) { - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin; @@ -99,7 +286,7 @@ public int getDecoratedMeasurement(View view) { @Override public int getDecoratedMeasurementInOther(View view) { - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin; @@ -107,14 +294,14 @@ public int getDecoratedMeasurementInOther(View view) { @Override public int getDecoratedEnd(View view) { - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedRight(view) + params.rightMargin; } @Override public int getDecoratedStart(View view) { - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedLeft(view) - params.leftMargin; } @@ -189,7 +376,7 @@ public int getStartAfterPadding() { @Override public int getDecoratedMeasurement(View view) { - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin; @@ -197,7 +384,7 @@ public int getDecoratedMeasurement(View view) { @Override public int getDecoratedMeasurementInOther(View view) { - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin; @@ -205,14 +392,14 @@ public int getDecoratedMeasurementInOther(View view) { @Override public int getDecoratedEnd(View view) { - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin; } @Override public int getDecoratedStart(View view) { - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedTop(view) - params.topMargin; } @@ -256,182 +443,4 @@ public int getModeInOther() { } }; } - - /** - * Returns the {@link RecyclerView.LayoutManager LayoutManager} that - * is associated with this OrientationHelper. - */ - public RecyclerView.LayoutManager getLayoutManager() { - return mLayoutManager; - } - - /** - * Call this method after onLayout method is complete if state is NOT pre-layout. - * This method records information like layout bounds that might be useful in the next layout - * calculations. - */ - public void onLayoutComplete() { - mLastTotalSpace = getTotalSpace(); - } - - /** - * Returns the layout space change between the previous layout pass and current layout pass. - *

    - * Make sure you call {@link #onLayoutComplete()} at the end of your LayoutManager's - * {@link RecyclerView.LayoutManager#onLayoutChildren(RecyclerView.Recycler, - * RecyclerView.State)} method. - * - * @return The difference between the current total space and previous layout's total space. - * @see #onLayoutComplete() - */ - public int getTotalSpaceChange() { - return INVALID_SIZE == mLastTotalSpace ? 0 : getTotalSpace() - mLastTotalSpace; - } - - /** - * Returns the start of the view including its decoration and margin. - *

    - * For example, for the horizontal helper, if a View's left is at pixel 20, has 2px left - * decoration and 3px left margin, returned value will be 15px. - * - * @param view The view element to check - * @return The first pixel of the element - * @see #getDecoratedEnd(android.view.View) - */ - public abstract int getDecoratedStart(View view); - - /** - * Returns the end of the view including its decoration and margin. - *

    - * For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right - * decoration and 3px right margin, returned value will be 205. - * - * @param view The view element to check - * @return The last pixel of the element - * @see #getDecoratedStart(android.view.View) - */ - public abstract int getDecoratedEnd(View view); - - /** - * Returns the end of the View after its matrix transformations are applied to its layout - * position. - *

    - * This method is useful when trying to detect the visible edge of a View. - *

    - * It includes the decorations but does not include the margins. - * - * @param view The view whose transformed end will be returned - * @return The end of the View after its decor insets and transformation matrix is applied to - * its position - * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect) - */ - public abstract int getTransformedEndWithDecoration(View view); - - /** - * Returns the start of the View after its matrix transformations are applied to its layout - * position. - *

    - * This method is useful when trying to detect the visible edge of a View. - *

    - * It includes the decorations but does not include the margins. - * - * @param view The view whose transformed start will be returned - * @return The start of the View after its decor insets and transformation matrix is applied to - * its position - * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect) - */ - public abstract int getTransformedStartWithDecoration(View view); - - /** - * Returns the space occupied by this View in the current orientation including decorations and - * margins. - * - * @param view The view element to check - * @return Total space occupied by this view - * @see #getDecoratedMeasurementInOther(View) - */ - public abstract int getDecoratedMeasurement(View view); - - /** - * Returns the space occupied by this View in the perpendicular orientation including - * decorations and margins. - * - * @param view The view element to check - * @return Total space occupied by this view in the perpendicular orientation to current one - * @see #getDecoratedMeasurement(View) - */ - public abstract int getDecoratedMeasurementInOther(View view); - - /** - * Returns the start position of the layout after the start padding is added. - * - * @return The very first pixel we can draw. - */ - public abstract int getStartAfterPadding(); - - /** - * Returns the end position of the layout after the end padding is removed. - * - * @return The end boundary for this layout. - */ - public abstract int getEndAfterPadding(); - - /** - * Returns the end position of the layout without taking padding into account. - * - * @return The end boundary for this layout without considering padding. - */ - public abstract int getEnd(); - - /** - * Offsets all children's positions by the given amount. - * - * @param amount Value to add to each child's layout parameters - */ - public abstract void offsetChildren(int amount); - - /** - * Returns the total space to layout. This number is the difference between - * {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}. - * - * @return Total space to layout children - */ - public abstract int getTotalSpace(); - - /** - * Offsets the child in this orientation. - * - * @param view View to offset - * @param offset offset amount - */ - public abstract void offsetChild(View view, int offset); - - /** - * Returns the padding at the end of the layout. For horizontal helper, this is the right - * padding and for vertical helper, this is the bottom padding. This method does not check - * whether the layout is RTL or not. - * - * @return The padding at the end of the layout. - */ - public abstract int getEndPadding(); - - /** - * Returns the MeasureSpec mode for the current orientation from the LayoutManager. - * - * @return The current measure spec mode. - * @see View.MeasureSpec - * @see RecyclerView.LayoutManager#getWidthMode() - * @see RecyclerView.LayoutManager#getHeightMode() - */ - public abstract int getMode(); - - /** - * Returns the MeasureSpec mode for the perpendicular orientation from the LayoutManager. - * - * @return The current measure spec mode. - * @see View.MeasureSpec - * @see RecyclerView.LayoutManager#getWidthMode() - * @see RecyclerView.LayoutManager#getHeightMode() - */ - public abstract int getModeInOther(); } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/PagerSnapHelper.java b/viewpager2/src/main/java/androidx/recyclerview/widget/PagerSnapHelper.java index d18bae012..3d97cdf55 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/PagerSnapHelper.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/PagerSnapHelper.java @@ -29,7 +29,7 @@ * horizontal orientation. * *

    - *

    + * * PagerSnapHelper can help achieve a similar behavior to * {@link androidx.viewpager.widget.ViewPager}. Set both {@link RecyclerView} and the items of the * {@link RecyclerView.Adapter} to have @@ -48,7 +48,7 @@ public class PagerSnapHelper extends SnapHelper { @Nullable @Override public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, - @NonNull View targetView) { + @NonNull View targetView) { int[] out = new int[2]; if (layoutManager.canScrollHorizontally()) { out[0] = distanceToCenter(targetView, @@ -81,13 +81,13 @@ public View findSnapView(RecyclerView.LayoutManager layoutManager) { @Override @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, - int velocityY) { - int itemCount = layoutManager.getItemCount(); + int velocityY) { + final int itemCount = layoutManager.getItemCount(); if (itemCount == 0) { return RecyclerView.NO_POSITION; } - OrientationHelper orientationHelper = getOrientationHelper(layoutManager); + final OrientationHelper orientationHelper = getOrientationHelper(layoutManager); if (orientationHelper == null) { return RecyclerView.NO_POSITION; } @@ -99,13 +99,13 @@ public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int int distanceAfter = Integer.MAX_VALUE; // Find the first view before the center, and the first view after the center - int childCount = layoutManager.getChildCount(); + final int childCount = layoutManager.getChildCount(); for (int i = 0; i < childCount; i++) { - View child = layoutManager.getChildAt(i); + final View child = layoutManager.getChildAt(i); if (child == null) { continue; } - int distance = distanceToCenter(child, orientationHelper); + final int distance = distanceToCenter(child, orientationHelper); if (distance <= 0 && distance > distanceBefore) { // Child is before the center and closer then the previous best @@ -120,7 +120,7 @@ public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int } // Return the position of the first child from the center, in the direction of the fling - boolean forwardDirection = isForwardFling(layoutManager, velocityX, velocityY); + final boolean forwardDirection = isForwardFling(layoutManager, velocityX, velocityY); if (forwardDirection && closestChildAfterCenter != null) { return layoutManager.getPosition(closestChildAfterCenter); } else if (!forwardDirection && closestChildBeforeCenter != null) { @@ -146,7 +146,7 @@ public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int } private boolean isForwardFling(RecyclerView.LayoutManager layoutManager, int velocityX, - int velocityY) { + int velocityY) { if (layoutManager.canScrollHorizontally()) { return velocityX > 0; } else { @@ -155,7 +155,7 @@ private boolean isForwardFling(RecyclerView.LayoutManager layoutManager, int vel } private boolean isReverseLayout(RecyclerView.LayoutManager layoutManager) { - int itemCount = layoutManager.getItemCount(); + final int itemCount = layoutManager.getItemCount(); if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider = (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager; @@ -177,12 +177,12 @@ protected RecyclerView.SmoothScroller createScroller( return new LinearSmoothScroller(mRecyclerView.getContext()) { @Override protected void onTargetFound(@NonNull View targetView, - @NonNull RecyclerView.State state, @NonNull Action action) { + @NonNull RecyclerView.State state, @NonNull Action action) { int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView); - int dx = snapDistances[0]; - int dy = snapDistances[1]; - int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))); + final int dx = snapDistances[0]; + final int dy = snapDistances[1]; + final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))); if (time > 0) { action.update(dx, dy, time, mDecelerateInterpolator); } @@ -201,9 +201,9 @@ protected int calculateTimeForScrolling(int dx) { } private int distanceToCenter(@NonNull View targetView, OrientationHelper helper) { - int childCenter = helper.getDecoratedStart(targetView) + final int childCenter = helper.getDecoratedStart(targetView) + (helper.getDecoratedMeasurement(targetView) / 2); - int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; + final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; return childCenter - containerCenter; } @@ -212,23 +212,24 @@ private int distanceToCenter(@NonNull View targetView, OrientationHelper helper) * * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached * {@link RecyclerView}. - * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}. + * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}. + * * @return the child view that is currently closest to the center of this parent. */ @Nullable private View findCenterView(RecyclerView.LayoutManager layoutManager, - OrientationHelper helper) { + OrientationHelper helper) { int childCount = layoutManager.getChildCount(); if (childCount == 0) { return null; } View closestChild = null; - int center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; + final int center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; int absClosest = Integer.MAX_VALUE; for (int i = 0; i < childCount; i++) { - View child = layoutManager.getChildAt(i); + final View child = layoutManager.getChildAt(i); int childCenter = helper.getDecoratedStart(child) + (helper.getDecoratedMeasurement(child) / 2); int absDistance = Math.abs(childCenter - center); diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/viewpager2/src/main/java/androidx/recyclerview/widget/RecyclerView.java index 76c15b352..a71ef172b 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/RecyclerView.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/RecyclerView.java @@ -72,6 +72,7 @@ import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.InputDeviceCompat; import androidx.core.view.MotionEventCompat; +import androidx.core.view.NestedScrollingChild2; import androidx.core.view.NestedScrollingChild3; import androidx.core.view.NestedScrollingChildHelper; import androidx.core.view.ScrollingView; @@ -210,55 +211,29 @@ * information about the Paging library, see the * library * documentation. - *

    + * * {@link androidx.recyclerview.R.attr#layoutManager} */ public class RecyclerView extends ViewGroup implements ScrollingView, - NestedScrollingChild3 { + NestedScrollingChild2, NestedScrollingChild3 { - public static final int HORIZONTAL = LinearLayout.HORIZONTAL; - public static final int VERTICAL = LinearLayout.VERTICAL; - public static final int NO_POSITION = -1; - public static final long NO_ID = -1; - public static final int INVALID_TYPE = -1; - /** - * Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates - * that the RecyclerView should use the standard touch slop for smooth, - * continuous scrolling. - */ - public static final int TOUCH_SLOP_DEFAULT = 0; - /** - * Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates - * that the RecyclerView should use the standard touch slop for scrolling - * widgets that snap to a page or other coarse-grained barrier. - */ - public static final int TOUCH_SLOP_PAGING = 1; - /** - * Constant that represents that a duration has not been defined. - */ - public static final int UNDEFINED_DURATION = Integer.MIN_VALUE; - /** - * The RecyclerView is not currently scrolling. - * - * @see #getScrollState() - */ - public static final int SCROLL_STATE_IDLE = 0; - /** - * The RecyclerView is currently being dragged by outside input such as user touch input. - * - * @see #getScrollState() - */ - public static final int SCROLL_STATE_DRAGGING = 1; - /** - * The RecyclerView is currently animating to a final position while not under - * outside control. - * - * @see #getScrollState() - */ - public static final int SCROLL_STATE_SETTLING = 2; static final String TAG = "RecyclerView"; + static final boolean DEBUG = false; + static final boolean VERBOSE_TRACING = false; + + private static final int[] NESTED_SCROLLING_ATTRS = + {16843830 /* android.R.attr.nestedScrollingEnabled */}; + + /** + * The following are copied from OverScroller to determine how far a fling will go. + */ + private static final float SCROLL_FRICTION = 0.015f; + private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) + private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); + private final float mPhysicalCoef; + /** * On Kitkat and JB MR2, there is a bug which prevents DisplayList from being invalidated if * a View is two levels deep(wrt to ViewHolder.itemView). DisplayList can be invalidated by @@ -274,70 +249,21 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * 0 when mode is unspecified. */ static final boolean ALLOW_SIZE_IN_UNSPECIFIED_SPEC = Build.VERSION.SDK_INT >= 23; + static final boolean POST_UPDATES_ON_ANIMATION = Build.VERSION.SDK_INT >= 16; + /** * On L+, with RenderThread, the UI thread has idle time after it has passed a frame off to * RenderThread but before the next frame begins. We schedule prefetch work in this window. */ static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21; - static final boolean DISPATCH_TEMP_DETACH = false; - static final int DEFAULT_ORIENTATION = VERTICAL; - static final int MAX_SCROLL_DURATION = 2000; - /** - * RecyclerView is calculating a scroll. - * If there are too many of these in Systrace, some Views inside RecyclerView might be causing - * it. Try to avoid using EditText, focusable views or handle them with care. - */ - static final String TRACE_SCROLL_TAG = "RV Scroll"; - /** - * RecyclerView is rebinding a View. - * If this is taking a lot of time, consider optimizing your layout or make sure you are not - * doing extra operations in onBindViewHolder call. - */ - static final String TRACE_BIND_VIEW_TAG = "RV OnBindView"; - /** - * RecyclerView is attempting to pre-populate off screen views. - */ - static final String TRACE_PREFETCH_TAG = "RV Prefetch"; - /** - * RecyclerView is attempting to pre-populate off screen itemviews within an off screen - * RecyclerView. - */ - static final String TRACE_NESTED_PREFETCH_TAG = "RV Nested Prefetch"; - /** - * RecyclerView is creating a new View. - * If too many of these present in Systrace: - * - There might be a problem in Recycling (e.g. custom Animations that set transient state and - * prevent recycling or ItemAnimator not implementing the contract properly. ({@link - * > Adapter#onFailedToRecycleView(ViewHolder)}) - *

    - * - There might be too many item view types. - * > Try merging them - *

    - * - There might be too many itemChange animations and not enough space in RecyclerPool. - * >Try increasing your pool size and item cache size. - */ - static final String TRACE_CREATE_VIEW_TAG = "RV CreateView"; - static final long FOREVER_NS = Long.MAX_VALUE; - static final Interpolator sQuinticInterpolator = t -> { - t -= 1.0f; - return t * t * t * t * t + 1.0f; - }; - static final StretchEdgeEffectFactory sDefaultEdgeEffectFactory = - new StretchEdgeEffectFactory(); - private static final int[] NESTED_SCROLLING_ATTRS = - {16843830 /* android.R.attr.nestedScrollingEnabled */}; - /** - * The following are copied from OverScroller to determine how far a fling will go. - */ - private static final float SCROLL_FRICTION = 0.015f; - private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) - private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); + /** * FocusFinder#findNextFocus is broken on ICS MR1 and older for View.FOCUS_BACKWARD direction. * We convert it to an absolute direction such as FOCUS_DOWN or FOCUS_LEFT. */ private static final boolean FORCE_ABS_FOCUS_SEARCH_DIRECTION = Build.VERSION.SDK_INT <= 15; + /** * on API 15-, a focused child can still be considered a focused child of RV even after * it's being removed or its focusable flag is set to false. This is because when this focused @@ -347,6 +273,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * side-effect. */ private static final boolean IGNORE_DETACHED_FOCUSED_CHILD = Build.VERSION.SDK_INT <= 15; + /** * When flinging the stretch towards scrolling content, it should destretch quicker than the * fling would normally do. The visual effect of flinging the stretch looks strange as little @@ -354,6 +281,52 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * scrolling quickly. */ private static final float FLING_DESTRETCH_FACTOR = 4f; + + static final boolean DISPATCH_TEMP_DETACH = false; + + /** @hide */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + @IntDef({HORIZONTAL, VERTICAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface Orientation { + } + + public static final int HORIZONTAL = LinearLayout.HORIZONTAL; + public static final int VERTICAL = LinearLayout.VERTICAL; + + static final int DEFAULT_ORIENTATION = VERTICAL; + public static final int NO_POSITION = -1; + public static final long NO_ID = -1; + public static final int INVALID_TYPE = -1; + + /** + * Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates + * that the RecyclerView should use the standard touch slop for smooth, + * continuous scrolling. + */ + public static final int TOUCH_SLOP_DEFAULT = 0; + + /** + * Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates + * that the RecyclerView should use the standard touch slop for scrolling + * widgets that snap to a page or other coarse-grained barrier. + */ + public static final int TOUCH_SLOP_PAGING = 1; + + /** + * Constant that represents that a duration has not been defined. + */ + public static final int UNDEFINED_DURATION = Integer.MIN_VALUE; + + static final int MAX_SCROLL_DURATION = 2000; + + /** + * RecyclerView is calculating a scroll. + * If there are too many of these in Systrace, some Views inside RecyclerView might be causing + * it. Try to avoid using EditText, focusable views or handle them with care. + */ + static final String TRACE_SCROLL_TAG = "RV Scroll"; + /** * OnLayout has been called by the View system. * If this shows up too many times in Systrace, make sure the children of RecyclerView do not @@ -361,6 +334,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * Adapter notifyItemChanged, RecyclerView can avoid full layout calculation. */ private static final String TRACE_ON_LAYOUT_TAG = "RV OnLayout"; + /** * NotifyDataSetChanged or equal has been called. * If this is taking a long time, try sending granular notify adapter changes instead of just @@ -368,6 +342,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * might help. */ private static final String TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG = "RV FullInvalidate"; + /** * RecyclerView is doing a layout for partial adapter updates (we know what has changed) * If this is taking a long time, you may have dispatched too many Adapter updates causing too @@ -375,96 +350,154 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * methods. */ private static final String TRACE_HANDLE_ADAPTER_UPDATES_TAG = "RV PartialInvalidate"; - private static final Class[] LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE = - new Class[]{Context.class, AttributeSet.class, int.class, int.class}; - private static final int INVALID_POINTER = -1; - final Recycler mRecycler = new Recycler(); + /** - * Keeps data about views to be used for animations + * RecyclerView is rebinding a View. + * If this is taking a lot of time, consider optimizing your layout or make sure you are not + * doing extra operations in onBindViewHolder call. */ - final ViewInfoStore mViewInfoStore = new ViewInfoStore(); - final Rect mTempRect = new Rect(); - final RectF mTempRectF = new RectF(); - // default access to avoid the need for synthetic accessors for Recycler inner class. - final List mRecyclerListeners = new ArrayList<>(); - final ArrayList mItemDecorations = new ArrayList<>(); - final ViewFlinger mViewFlinger = new ViewFlinger(); - final State mState = new State(); - // Reusable int array to be passed to method calls that mutate it in order to "return" two ints. - final int[] mReusableIntPair = new int[2]; + static final String TRACE_BIND_VIEW_TAG = "RV OnBindView"; + /** - * These are views that had their a11y importance changed during a layout. We defer these events - * until the end of the layout because a11y service may make sync calls back to the RV while - * the View's state is undefined. + * RecyclerView is attempting to pre-populate off screen views. */ - @VisibleForTesting - final List mPendingAccessibilityImportanceChange = new ArrayList<>(); - final boolean mEnableFastScroller; - final GapWorker.LayoutPrefetchRegistryImpl mPrefetchRegistry = - ALLOW_THREAD_GAP_WORK ? new GapWorker.LayoutPrefetchRegistryImpl() : null; - private final float mPhysicalCoef; + static final String TRACE_PREFETCH_TAG = "RV Prefetch"; + + /** + * RecyclerView is attempting to pre-populate off screen itemviews within an off screen + * RecyclerView. + */ + static final String TRACE_NESTED_PREFETCH_TAG = "RV Nested Prefetch"; + + /** + * RecyclerView is creating a new View. + * If too many of these present in Systrace: + * - There might be a problem in Recycling (e.g. custom Animations that set transient state and + * prevent recycling or ItemAnimator not implementing the contract properly. ({@link + * > Adapter#onFailedToRecycleView(ViewHolder)}) + * + * - There might be too many item view types. + * > Try merging them + * + * - There might be too many itemChange animations and not enough space in RecyclerPool. + * >Try increasing your pool size and item cache size. + */ + static final String TRACE_CREATE_VIEW_TAG = "RV CreateView"; + private static final Class[] LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE = + new Class[]{Context.class, AttributeSet.class, int.class, int.class}; + private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver(); - private final Rect mTempRect2 = new Rect(); - private final ArrayList mOnItemTouchListeners = - new ArrayList<>(); - private final AccessibilityManager mAccessibilityManager; - private final int mMinFlingVelocity; - private final int mMaxFlingVelocity; - // This value is used when handling rotary encoder generic motion events. - private final float mScaledHorizontalScrollFactor; - private final float mScaledVerticalScrollFactor; - private final ItemAnimator.ItemAnimatorListener mItemAnimatorListener = - new ItemAnimatorRestoreListener(); - // simple array to keep min and max child position during a layout calculation - // preserved not to create a new one in each layout pass - private final int[] mMinMaxLayoutPositions = new int[2]; - private final int[] mScrollOffset = new int[2]; - private final int[] mNestedOffsets = new int[2]; + + final Recycler mRecycler = new Recycler(); + SavedState mPendingSavedState; + /** * Handles adapter updates */ AdapterHelper mAdapterHelper; + /** * Handles abstraction between LayoutManager children and RecyclerView children */ ChildHelper mChildHelper; + + /** + * Keeps data about views to be used for animations + */ + final ViewInfoStore mViewInfoStore = new ViewInfoStore(); + /** * Prior to L, there is no way to query this variable which is why we override the setter and * track it here. */ boolean mClipToPadding; - Adapter mAdapter; - @VisibleForTesting - LayoutManager mLayout; - // TODO: Remove this once setRecyclerListener has been removed. - RecyclerListener mRecyclerListener; - boolean mIsAttached; - boolean mHasFixedSize; - @VisibleForTesting - boolean mFirstLayoutComplete; - /** - * True if a call to requestLayout was intercepted and prevented from executing like normal and - * we plan on continuing with normal execution later. - */ - boolean mLayoutWasDefered; - boolean mLayoutSuppressed; - // Touch/scrolling handling - boolean mAdapterUpdateDuringMeasure; + /** + * Note: this Runnable is only ever posted if: + * 1) We've been through first layout + * 2) We know we have a fixed size (mHasFixedSize) + * 3) We're attached + */ + final Runnable mUpdateChildViewsRunnable = new Runnable() { + @Override + public void run() { + if (!mFirstLayoutComplete || isLayoutRequested()) { + // a layout request will happen, we should not do layout here. + return; + } + if (!mIsAttached) { + requestLayout(); + // if we are not attached yet, mark us as requiring layout and skip + return; + } + if (mLayoutSuppressed) { + mLayoutWasDefered = true; + return; //we'll process updates when ice age ends. + } + consumePendingUpdateOperations(); + } + }; + + final Rect mTempRect = new Rect(); + private final Rect mTempRect2 = new Rect(); + final RectF mTempRectF = new RectF(); + Adapter mAdapter; + @VisibleForTesting + LayoutManager mLayout; + // TODO: Remove this once setRecyclerListener has been removed. + RecyclerListener mRecyclerListener; + // default access to avoid the need for synthetic accessors for Recycler inner class. + final List mRecyclerListeners = new ArrayList<>(); + final ArrayList mItemDecorations = new ArrayList<>(); + private final ArrayList mOnItemTouchListeners = + new ArrayList<>(); + private OnItemTouchListener mInterceptingOnItemTouchListener; + boolean mIsAttached; + boolean mHasFixedSize; + boolean mEnableFastScroller; + @VisibleForTesting + boolean mFirstLayoutComplete; + + /** + * The current depth of nested calls to {@link #startInterceptRequestLayout()} (number of + * calls to {@link #startInterceptRequestLayout()} - number of calls to + * {@link #stopInterceptRequestLayout(boolean)} . This is used to signal whether we + * should defer layout operations caused by layout requests from children of + * {@link RecyclerView}. + */ + private int mInterceptRequestLayoutDepth = 0; + + /** + * True if a call to requestLayout was intercepted and prevented from executing like normal and + * we plan on continuing with normal execution later. + */ + boolean mLayoutWasDefered; + + boolean mLayoutSuppressed; + private boolean mIgnoreMotionEventTillDown; + + // binary OR of change events that were eaten during a layout or scroll. + private int mEatenAccessibilityChangeFlags; + boolean mAdapterUpdateDuringMeasure; + + private final AccessibilityManager mAccessibilityManager; + private List mOnChildAttachStateListeners; + /** * True after an event occurs that signals that the entire data set has changed. In that case, * we cannot run any animations since we don't know what happened until layout. - *

    + * * Attached items are invalid until next layout, at which point layout will animate/replace * items as necessary, building up content from the (effectively) new adapter from scratch. - *

    + * * Cached items must be discarded when setting this to true, so that the cache may be freely * used by prefetching until the next layout occurs. * * @see #processDataSetCompletelyChanged(boolean) */ - boolean mDataSetHasChangedAfterLayout; + boolean mDataSetHasChangedAfterLayout = false; + /** * True after the data set has completely changed and * {@link LayoutManager#onItemsChanged(RecyclerView)} should be called during the subsequent @@ -472,73 +505,8 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * * @see #processDataSetCompletelyChanged(boolean) */ - boolean mDispatchItemsChangedEvent; - ItemAnimator mItemAnimator = new DefaultItemAnimator(); - GapWorker mGapWorker; - // For use in item animations - boolean mItemsAddedOrRemoved; - boolean mItemsChanged; - boolean mPostedAnimatorRunner; - private final Runnable mItemAnimatorRunner = () -> { - if (mItemAnimator != null) { - mItemAnimator.runPendingAnimations(); - } - mPostedAnimatorRunner = false; - }; - /** - * The callback to convert view info diffs into animations. - */ - private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback = - new ViewInfoStore.ProcessCallback() { - @Override - public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info, - @Nullable ItemHolderInfo postInfo) { - mRecycler.unscrapView(viewHolder); - animateDisappearance(viewHolder, info, postInfo); - } - - @Override - public void processAppeared(ViewHolder viewHolder, - ItemHolderInfo preInfo, ItemHolderInfo info) { - animateAppearance(viewHolder, preInfo, info); - } - - @Override - public void processPersistent(ViewHolder viewHolder, - @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) { - viewHolder.setIsRecyclable(false); - if (mDataSetHasChangedAfterLayout) { - // since it was rebound, use change instead as we'll be mapping them from - // stable ids. If stable ids were false, we would not be running any - // animations - if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, - postInfo)) { - postAnimationRunner(); - } - } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) { - postAnimationRunner(); - } - } + boolean mDispatchItemsChangedEvent = false; - @Override - public void unused(ViewHolder viewHolder) { - mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler); - } - }; - RecyclerViewAccessibilityDelegate mAccessibilityDelegate; - private OnItemTouchListener mInterceptingOnItemTouchListener; - /** - * The current depth of nested calls to {@link #startInterceptRequestLayout()} (number of - * calls to {@link #startInterceptRequestLayout()} - number of calls to - * {@link #stopInterceptRequestLayout(boolean)} . This is used to signal whether we - * should defer layout operations caused by layout requests from children of - * {@link RecyclerView}. - */ - private int mInterceptRequestLayoutDepth; - private boolean mIgnoreMotionEventTillDown; - // binary OR of change events that were eaten during a layout or scroll. - private int mEatenAccessibilityChangeFlags; - private List mOnChildAttachStateListeners; /** * This variable is incremented during a dispatchLayout and/or scroll. * Some methods should not be called during these periods (e.g. adapter data change). @@ -547,7 +515,8 @@ public void unused(ViewHolder viewHolder) { * @see #assertInLayoutOrScroll(String) * @see #assertNotInLayoutOrScroll(String) */ - private int mLayoutOrScrollCounter; + private int mLayoutOrScrollCounter = 0; + /** * Similar to mLayoutOrScrollCounter but logs a warning instead of throwing an exception * (for API compatibility). @@ -555,10 +524,42 @@ public void unused(ViewHolder viewHolder) { * It is a bad practice for a developer to update the data in a scroll callback since it is * potentially called during a layout. */ - private int mDispatchScrollCounter; + private int mDispatchScrollCounter = 0; + @NonNull private EdgeEffectFactory mEdgeEffectFactory = sDefaultEdgeEffectFactory; private EdgeEffect mLeftGlow, mTopGlow, mRightGlow, mBottomGlow; + + ItemAnimator mItemAnimator = new DefaultItemAnimator(); + + private static final int INVALID_POINTER = -1; + + /** + * The RecyclerView is not currently scrolling. + * + * @see #getScrollState() + */ + public static final int SCROLL_STATE_IDLE = 0; + + /** + * The RecyclerView is currently being dragged by outside input such as user touch input. + * + * @see #getScrollState() + */ + public static final int SCROLL_STATE_DRAGGING = 1; + + /** + * The RecyclerView is currently animating to a final position while not under + * outside control. + * + * @see #getScrollState() + */ + public static final int SCROLL_STATE_SETTLING = 2; + + static final long FOREVER_NS = Long.MAX_VALUE; + + // Touch/scrolling handling + private int mScrollState = SCROLL_STATE_IDLE; private int mScrollPointerId = INVALID_POINTER; private VelocityTracker mVelocityTracker; @@ -568,11 +569,69 @@ public void unused(ViewHolder viewHolder) { private int mLastTouchY; private int mTouchSlop; private OnFlingListener mOnFlingListener; + private final int mMinFlingVelocity; + private final int mMaxFlingVelocity; + + // This value is used when handling rotary encoder generic motion events. + private float mScaledHorizontalScrollFactor = Float.MIN_VALUE; + private float mScaledVerticalScrollFactor = Float.MIN_VALUE; + private boolean mPreserveFocusAfterLayout = true; + + final ViewFlinger mViewFlinger = new ViewFlinger(); + + GapWorker mGapWorker; + GapWorker.LayoutPrefetchRegistryImpl mPrefetchRegistry = + ALLOW_THREAD_GAP_WORK ? new GapWorker.LayoutPrefetchRegistryImpl() : null; + + final State mState = new State(); + private OnScrollListener mScrollListener; private List mScrollListeners; + + // For use in item animations + boolean mItemsAddedOrRemoved = false; + boolean mItemsChanged = false; + private ItemAnimator.ItemAnimatorListener mItemAnimatorListener = + new ItemAnimatorRestoreListener(); + boolean mPostedAnimatorRunner = false; + RecyclerViewAccessibilityDelegate mAccessibilityDelegate; private ChildDrawingOrderCallback mChildDrawingOrderCallback; + + // simple array to keep min and max child position during a layout calculation + // preserved not to create a new one in each layout pass + private final int[] mMinMaxLayoutPositions = new int[2]; + private NestedScrollingChildHelper mScrollingChildHelper; + private final int[] mScrollOffset = new int[2]; + private final int[] mNestedOffsets = new int[2]; + + // Reusable int array to be passed to method calls that mutate it in order to "return" two ints. + final int[] mReusableIntPair = new int[2]; + + /** + * These are views that had their a11y importance changed during a layout. We defer these events + * until the end of the layout because a11y service may make sync calls back to the RV while + * the View's state is undefined. + */ + @VisibleForTesting + final List mPendingAccessibilityImportanceChange = new ArrayList<>(); + + private Runnable mItemAnimatorRunner = () -> { + if (mItemAnimator != null) { + mItemAnimator.runPendingAnimations(); + } + mPostedAnimatorRunner = false; + }; + + static final Interpolator sQuinticInterpolator = t -> { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + }; + + static final StretchEdgeEffectFactory sDefaultEdgeEffectFactory = + new StretchEdgeEffectFactory(); + // These fields are only used to track whether we need to layout and measure RV children in // onLayout. // @@ -592,48 +651,64 @@ public void unused(ViewHolder viewHolder) { // layout, we can see if our last known measurement information is different from our actual // laid out size, and if it is, only then do we remeasure and relayout children. private boolean mLastAutoMeasureSkippedDueToExact; - private int mLastAutoMeasureNonExactMeasuredWidth; - private int mLastAutoMeasureNonExactMeasuredHeight; + private int mLastAutoMeasureNonExactMeasuredWidth = 0; + private int mLastAutoMeasureNonExactMeasuredHeight = 0; + /** - * Note: this Runnable is only ever posted if: - * 1) We've been through first layout - * 2) We know we have a fixed size (mHasFixedSize) - * 3) We're attached + * The callback to convert view info diffs into animations. */ - final Runnable mUpdateChildViewsRunnable = new Runnable() { - @Override - public void run() { - if (!mFirstLayoutComplete || isLayoutRequested()) { - // a layout request will happen, we should not do layout here. - return; - } - if (!mIsAttached) { - requestLayout(); - // if we are not attached yet, mark us as requiring layout and skip - return; - } - if (mLayoutSuppressed) { - mLayoutWasDefered = true; - return; //we'll process updates when ice age ends. - } - consumePendingUpdateOperations(); - } - }; + private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback = + new ViewInfoStore.ProcessCallback() { + @Override + public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info, + @Nullable ItemHolderInfo postInfo) { + mRecycler.unscrapView(viewHolder); + animateDisappearance(viewHolder, info, postInfo); + } - public RecyclerView(@NonNull Context context) { - this(context, null); - } + @Override + public void processAppeared(ViewHolder viewHolder, + ItemHolderInfo preInfo, ItemHolderInfo info) { + animateAppearance(viewHolder, preInfo, info); + } - public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { - this(context, attrs, R.attr.recyclerViewStyle); - } + @Override + public void processPersistent(ViewHolder viewHolder, + @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) { + viewHolder.setIsRecyclable(false); + if (mDataSetHasChangedAfterLayout) { + // since it was rebound, use change instead as we'll be mapping them from + // stable ids. If stable ids were false, we would not be running any + // animations + if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, + postInfo)) { + postAnimationRunner(); + } + } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) { + postAnimationRunner(); + } + } + + @Override + public void unused(ViewHolder viewHolder) { + mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler); + } + }; + + public RecyclerView(@NonNull Context context) { + this(context, null); + } + + public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, R.attr.recyclerViewStyle); + } public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setScrollContainer(true); setFocusableInTouchMode(true); - ViewConfiguration vc = ViewConfiguration.get(context); + final ViewConfiguration vc = ViewConfiguration.get(context); mTouchSlop = vc.getScaledTouchSlop(); mScaledHorizontalScrollFactor = ViewConfigurationCompat.getScaledHorizontalScrollFactor(vc, context); @@ -641,7 +716,7 @@ public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int ViewConfigurationCompat.getScaledVerticalScrollFactor(vc, context); mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); - float ppi = context.getResources().getDisplayMetrics().density * 160.0f; + final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; mPhysicalCoef = SensorManager.GRAVITY_EARTH // g (m/s^2) * 39.37f // inch/meter * ppi @@ -706,73 +781,12 @@ public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int PoolingContainer.setPoolingContainer(this, true); } - static ViewHolder getChildViewHolderInt(View child) { - if (child == null) { - return null; - } - return ((LayoutParams) child.getLayoutParams()).mViewHolder; - } - - static void getDecoratedBoundsWithMarginsInt(View view, Rect outBounds) { - LayoutParams lp = (LayoutParams) view.getLayoutParams(); - Rect insets = lp.mDecorInsets; - outBounds.set(view.getLeft() - insets.left - lp.leftMargin, - view.getTop() - insets.top - lp.topMargin, - view.getRight() + insets.right + lp.rightMargin, - view.getBottom() + insets.bottom + lp.bottomMargin); - } - - /** - * Utility method for finding an internal RecyclerView, if present - */ - @Nullable - static RecyclerView findNestedRecyclerView(@NonNull View view) { - if (!(view instanceof ViewGroup)) { - return null; - } - if (view instanceof RecyclerView) { - return (RecyclerView) view; - } - ViewGroup parent = (ViewGroup) view; - int count = parent.getChildCount(); - for (int i = 0; i < count; i++) { - View child = parent.getChildAt(i); - RecyclerView descendant = findNestedRecyclerView(child); - if (descendant != null) { - return descendant; - } - } - return null; - } - - /** - * Utility method for clearing holder's internal RecyclerView, if present - */ - static void clearNestedRecyclerViewIfNotNested(@NonNull ViewHolder holder) { - if (holder.mNestedRecyclerView != null) { - View item = holder.mNestedRecyclerView.get(); - while (item != null) { - if (item == holder.itemView) { - return; // match found, don't need to clear - } - - ViewParent parent = item.getParent(); - if (parent instanceof View) { - item = (View) parent; - } else { - item = null; - } - } - holder.mNestedRecyclerView = null; // not nested - } - } - /** * Label appended to all public exception strings, used to help find which RV in an app is * hitting an exception. */ String exceptionLabel() { - return " " + this + return " " + super.toString() + ", adapter:" + mAdapter + ", layout:" + mLayout + ", context:" + getContext(); @@ -822,7 +836,7 @@ public CharSequence getAccessibilityClassName() { * Instantiate and set a LayoutManager, if specified in the attributes. */ private void createLayoutManager(Context context, String className, AttributeSet attrs, - int defStyleAttr, int defStyleRes) { + int defStyleAttr, int defStyleRes) { if (className != null) { className = className.trim(); if (!className.isEmpty()) { @@ -831,7 +845,7 @@ private void createLayoutManager(Context context, String className, AttributeSet ClassLoader classLoader; if (isInEditMode()) { // Stupid layoutlib cannot handle simple class loaders. - classLoader = getClass().getClassLoader(); + classLoader = this.getClass().getClassLoader(); } else { classLoader = context.getClassLoader(); } @@ -908,7 +922,7 @@ public int indexOfChild(View view) { @Override public void removeViewAt(int index) { - View child = RecyclerView.this.getChildAt(index); + final View child = RecyclerView.this.getChildAt(index); if (child != null) { dispatchChildDetached(child); @@ -933,7 +947,7 @@ public View getChildAt(int offset) { @Override public void removeAllViews() { - int count = getChildCount(); + final int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); dispatchChildDetached(child); @@ -953,8 +967,8 @@ public ViewHolder getChildViewHolder(View view) { @Override public void attachViewToParent(View child, int index, - ViewGroup.LayoutParams layoutParams) { - ViewHolder vh = getChildViewHolderInt(child); + ViewGroup.LayoutParams layoutParams) { + final ViewHolder vh = getChildViewHolderInt(child); if (vh != null) { if (!vh.isTmpDetached() && !vh.shouldIgnore()) { throw new IllegalArgumentException("Called attach on a child which is not" @@ -970,9 +984,9 @@ public void attachViewToParent(View child, int index, @Override public void detachViewFromParent(int offset) { - View view = getChildAt(offset); + final View view = getChildAt(offset); if (view != null) { - ViewHolder vh = getChildViewHolderInt(view); + final ViewHolder vh = getChildViewHolderInt(view); if (vh != null) { if (vh.isTmpDetached() && !vh.shouldIgnore()) { throw new IllegalArgumentException("called detach on an already" @@ -989,7 +1003,7 @@ public void detachViewFromParent(int offset) { @Override public void onEnteredHiddenState(View child) { - ViewHolder vh = getChildViewHolderInt(child); + final ViewHolder vh = getChildViewHolderInt(child); if (vh != null) { vh.onEnteredHiddenState(RecyclerView.this); } @@ -997,7 +1011,7 @@ public void onEnteredHiddenState(View child) { @Override public void onLeftHiddenState(View child) { - ViewHolder vh = getChildViewHolderInt(child); + final ViewHolder vh = getChildViewHolderInt(child); if (vh != null) { vh.onLeftHiddenState(RecyclerView.this); } @@ -1009,7 +1023,7 @@ void initAdapterManager() { mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() { @Override public ViewHolder findViewHolder(int position) { - ViewHolder vh = findViewHolderForPosition(position, true); + final ViewHolder vh = findViewHolderForPosition(position, true); if (vh == null) { return null; } @@ -1111,6 +1125,18 @@ public boolean hasFixedSize() { return mHasFixedSize; } + @Override + public void setClipToPadding(boolean clipToPadding) { + if (clipToPadding != mClipToPadding) { + invalidateGlows(); + } + mClipToPadding = clipToPadding; + super.setClipToPadding(clipToPadding); + if (mFirstLayoutComplete) { + requestLayout(); + } + } + /** * Returns whether this RecyclerView will clip its children to its padding, and resize (but * not clip) any EdgeEffect to the padded region, if padding is present. @@ -1127,21 +1153,9 @@ public boolean getClipToPadding() { return mClipToPadding; } - @Override - public void setClipToPadding(boolean clipToPadding) { - if (clipToPadding != mClipToPadding) { - invalidateGlows(); - } - mClipToPadding = clipToPadding; - super.setClipToPadding(clipToPadding); - if (mFirstLayoutComplete) { - requestLayout(); - } - } - /** * Configure the scrolling touch slop for a specific use case. - *

    + * * Set up the RecyclerView's scrolling motion threshold based on common usages. * Valid arguments are {@link #TOUCH_SLOP_DEFAULT} and {@link #TOUCH_SLOP_PAGING}. * @@ -1149,7 +1163,7 @@ public void setClipToPadding(boolean clipToPadding) { * the intended usage of this RecyclerView */ public void setScrollingTouchSlop(int slopConstant) { - ViewConfiguration vc = ViewConfiguration.get(getContext()); + final ViewConfiguration vc = ViewConfiguration.get(getContext()); switch (slopConstant) { default: Log.w(TAG, "setScrollingTouchSlop(): bad argument constant " @@ -1187,6 +1201,23 @@ public void swapAdapter(@Nullable Adapter adapter, boolean removeAndRecycleExist requestLayout(); } + /** + * Set a new adapter to provide child views on demand. + *

    + * When adapter is changed, all existing views are recycled back to the pool. If the pool has + * only one adapter, it will be cleared. + * + * @param adapter The new adapter to set, or null to set no adapter. + * @see #swapAdapter(Adapter, boolean) + */ + public void setAdapter(@Nullable Adapter adapter) { + // bail out if layout is frozen + setLayoutFrozen(false); + setAdapterInternal(adapter, false, true); + processDataSetCompletelyChanged(false); + requestLayout(); + } + /** * Removes and recycles all views - both those currently attached, and those in the Recycler. */ @@ -1218,7 +1249,7 @@ void removeAndRecycleViews() { * compatibleWithPrevious is false, this parameter is ignored. */ private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious, - boolean removeAndRecycleViews) { + boolean removeAndRecycleViews) { if (mAdapter != null) { mAdapter.unregisterAdapterDataObserver(mObserver); mAdapter.onDetachedFromRecyclerView(this); @@ -1227,7 +1258,7 @@ private void setAdapterInternal(@Nullable Adapter adapter, boolean compatible removeAndRecycleViews(); } mAdapterHelper.reset(); - Adapter oldAdapter = mAdapter; + final Adapter oldAdapter = mAdapter; mAdapter = adapter; if (adapter != null) { adapter.registerAdapterDataObserver(mObserver); @@ -1251,23 +1282,6 @@ public Adapter getAdapter() { return mAdapter; } - /** - * Set a new adapter to provide child views on demand. - *

    - * When adapter is changed, all existing views are recycled back to the pool. If the pool has - * only one adapter, it will be cleared. - * - * @param adapter The new adapter to set, or null to set no adapter. - * @see #swapAdapter(Adapter, boolean) - */ - public void setAdapter(@Nullable Adapter adapter) { - // bail out if layout is frozen - setLayoutFrozen(false); - setAdapterInternal(adapter, false, true); - processDataSetCompletelyChanged(false); - requestLayout(); - } - /** * Register a listener that will be notified whenever a child view is recycled. * @@ -1278,7 +1292,7 @@ public void setAdapter(@Nullable Adapter adapter) { * * @param listener Listener to register, or null to clear * @deprecated Use {@link #addRecyclerListener(RecyclerListener)} and - * {@link #removeRecyclerListener(RecyclerListener)} + * {@link #removeRecyclerListener(RecyclerListener)} */ @Deprecated public void setRecyclerListener(@Nullable RecyclerListener listener) { @@ -1370,13 +1384,57 @@ public void clearOnChildAttachStateChangeListeners() { } /** - * Get the current {@link OnFlingListener} from this {@link RecyclerView}. + * Set the {@link LayoutManager} that this RecyclerView will use. * - * @return The {@link OnFlingListener} instance currently set (can be null). + *

    In contrast to other adapter-backed views such as {@link android.widget.ListView} + * or {@link android.widget.GridView}, RecyclerView allows client code to provide custom + * layout arrangements for child views. These arrangements are controlled by the + * {@link LayoutManager}. A LayoutManager must be provided for RecyclerView to function.

    + * + *

    Several default strategies are provided for common uses such as lists and grids.

    + * + * @param layout LayoutManager to use */ - @Nullable - public OnFlingListener getOnFlingListener() { - return mOnFlingListener; + public void setLayoutManager(@Nullable LayoutManager layout) { + if (layout == mLayout) { + return; + } + stopScroll(); + // TODO We should do this switch a dispatchLayout pass and animate children. There is a good + // chance that LayoutManagers will re-use views. + if (mLayout != null) { + // end all running animations + if (mItemAnimator != null) { + mItemAnimator.endAnimations(); + } + mLayout.removeAndRecycleAllViews(mRecycler); + mLayout.removeAndRecycleScrapInt(mRecycler); + mRecycler.clear(); + + if (mIsAttached) { + mLayout.dispatchDetachedFromWindow(this, mRecycler); + } + mLayout.setRecyclerView(null); + mLayout = null; + } else { + mRecycler.clear(); + } + // this is just a defensive measure for faulty item animators. + mChildHelper.removeAllViewsUnfiltered(); + mLayout = layout; + if (layout != null) { + if (layout.mRecyclerView != null) { + throw new IllegalArgumentException("LayoutManager " + layout + + " is already attached to a RecyclerView:" + + layout.mRecyclerView.exceptionLabel()); + } + mLayout.setRecyclerView(this); + if (mIsAttached) { + mLayout.dispatchAttachedToWindow(this); + } + } + mRecycler.updateViewCacheSize(); + requestLayout(); } /** @@ -1391,6 +1449,16 @@ public void setOnFlingListener(@Nullable OnFlingListener onFlingListener) { mOnFlingListener = onFlingListener; } + /** + * Get the current {@link OnFlingListener} from this {@link RecyclerView}. + * + * @return The {@link OnFlingListener} instance currently set (can be null). + */ + @Nullable + public OnFlingListener getOnFlingListener() { + return mOnFlingListener; + } + @Override protected Parcelable onSaveInstanceState() { SavedState state = new SavedState(super.onSaveInstanceState()); @@ -1450,8 +1518,8 @@ protected void dispatchRestoreInstanceState(SparseArray container) { * @param viewHolder The ViewHolder to be removed */ private void addAnimatingView(ViewHolder viewHolder) { - View view = viewHolder.itemView; - boolean alreadyParented = view.getParent() == this; + final View view = viewHolder.itemView; + final boolean alreadyParented = view.getParent() == this; mRecycler.unscrapView(getChildViewHolder(view)); if (viewHolder.isTmpDetached()) { // re-attach @@ -1472,9 +1540,9 @@ private void addAnimatingView(ViewHolder viewHolder) { */ boolean removeAnimatingView(View view) { startInterceptRequestLayout(); - boolean removed = mChildHelper.removeViewIfHidden(view); + final boolean removed = mChildHelper.removeViewIfHidden(view); if (removed) { - ViewHolder viewHolder = getChildViewHolderInt(view); + final ViewHolder viewHolder = getChildViewHolderInt(view); mRecycler.unscrapView(viewHolder); mRecycler.recycleViewHolderInternal(viewHolder); if (DEBUG) { @@ -1498,83 +1566,29 @@ public LayoutManager getLayoutManager() { } /** - * Set the {@link LayoutManager} that this RecyclerView will use. - * - *

    In contrast to other adapter-backed views such as {@link android.widget.ListView} - * or {@link android.widget.GridView}, RecyclerView allows client code to provide custom - * layout arrangements for child views. These arrangements are controlled by the - * {@link LayoutManager}. A LayoutManager must be provided for RecyclerView to function.

    + * Retrieve this RecyclerView's {@link RecycledViewPool}. This method will never return null; + * if no pool is set for this view a new one will be created. See + * {@link #setRecycledViewPool(RecycledViewPool) setRecycledViewPool} for more information. * - *

    Several default strategies are provided for common uses such as lists and grids.

    + * @return The pool used to store recycled item views for reuse. + * @see #setRecycledViewPool(RecycledViewPool) + */ + @NonNull + public RecycledViewPool getRecycledViewPool() { + return mRecycler.getRecycledViewPool(); + } + + /** + * Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views. + * This can be useful if you have multiple RecyclerViews with adapters that use the same + * view types, for example if you have several data sets with the same kinds of item views + * displayed by a {@link androidx.viewpager.widget.ViewPager}. * - * @param layout LayoutManager to use + * @param pool Pool to set. If this parameter is null a new pool will be created and used. */ - public void setLayoutManager(@Nullable LayoutManager layout) { - if (layout == mLayout) { - return; - } - stopScroll(); - // TODO We should do this switch a dispatchLayout pass and animate children. There is a good - // chance that LayoutManagers will re-use views. - if (mLayout != null) { - // end all running animations - if (mItemAnimator != null) { - mItemAnimator.endAnimations(); - } - mLayout.removeAndRecycleAllViews(mRecycler); - mLayout.removeAndRecycleScrapInt(mRecycler); - mRecycler.clear(); - - if (mIsAttached) { - mLayout.dispatchDetachedFromWindow(this, mRecycler); - } - mLayout.setRecyclerView(null); - mLayout = null; - } else { - mRecycler.clear(); - } - // this is just a defensive measure for faulty item animators. - mChildHelper.removeAllViewsUnfiltered(); - mLayout = layout; - if (layout != null) { - if (layout.mRecyclerView != null) { - throw new IllegalArgumentException("LayoutManager " + layout - + " is already attached to a RecyclerView:" - + layout.mRecyclerView.exceptionLabel()); - } - mLayout.setRecyclerView(this); - if (mIsAttached) { - mLayout.dispatchAttachedToWindow(this); - } - } - mRecycler.updateViewCacheSize(); - requestLayout(); - } - - /** - * Retrieve this RecyclerView's {@link RecycledViewPool}. This method will never return null; - * if no pool is set for this view a new one will be created. See - * {@link #setRecycledViewPool(RecycledViewPool) setRecycledViewPool} for more information. - * - * @return The pool used to store recycled item views for reuse. - * @see #setRecycledViewPool(RecycledViewPool) - */ - @NonNull - public RecycledViewPool getRecycledViewPool() { - return mRecycler.getRecycledViewPool(); - } - - /** - * Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views. - * This can be useful if you have multiple RecyclerViews with adapters that use the same - * view types, for example if you have several data sets with the same kinds of item views - * displayed by a {@link androidx.viewpager.widget.ViewPager}. - * - * @param pool Pool to set. If this parameter is null a new pool will be created and used. - */ - public void setRecycledViewPool(@Nullable RecycledViewPool pool) { - mRecycler.setRecycledViewPool(pool); - } + public void setRecycledViewPool(@Nullable RecycledViewPool pool) { + mRecycler.setRecycledViewPool(pool); + } /** * Sets a new {@link ViewCacheExtension} to be used by the Recycler. @@ -1682,7 +1696,7 @@ public void addItemDecoration(@NonNull ItemDecoration decor) { */ @NonNull public ItemDecoration getItemDecorationAt(int index) { - int size = getItemDecorationCount(); + final int size = getItemDecorationCount(); if (index < 0 || index >= size) { throw new IndexOutOfBoundsException(index + " is an invalid index for size " + size); } @@ -1705,7 +1719,7 @@ public int getItemDecorationCount() { * @param index The index position of the ItemDecoration to be removed. */ public void removeItemDecorationAt(int index) { - int size = getItemDecorationCount(); + final int size = getItemDecorationCount(); if (index < 0 || index >= size) { throw new IndexOutOfBoundsException(index + " is an invalid index for size " + size); } @@ -1806,7 +1820,7 @@ public void clearOnScrollListeners() { /** * Convenience method to scroll to a certain position. - *

    + * * RecyclerView does not implement scrolling logic, rather forwards the call to * {@link RecyclerView.LayoutManager#scrollToPosition(int)} * @@ -1882,8 +1896,8 @@ public void scrollBy(int x, int y) { if (mLayoutSuppressed) { return; } - boolean canScrollHorizontal = mLayout.canScrollHorizontally(); - boolean canScrollVertical = mLayout.canScrollVertically(); + final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); + final boolean canScrollVertical = mLayout.canScrollVertically(); if (canScrollHorizontal || canScrollVertical) { scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null, TYPE_TOUCH); @@ -1892,9 +1906,8 @@ public void scrollBy(int x, int y) { /** * Same as {@link RecyclerView#scrollBy(int, int)}, but also participates in nested scrolling. - * - * @param x The amount of horizontal scroll requested - * @param y The amount of vertical scroll requested + * @param x The amount of horizontal scroll requested + * @param y The amount of vertical scroll requested * @see androidx.core.view.NestedScrollingChild */ public void nestedScrollBy(int x, int y) { @@ -1905,11 +1918,10 @@ public void nestedScrollBy(int x, int y) { * Similar to {@link RecyclerView#scrollByInternal(int, int, MotionEvent, int)}, but fully * participates in nested scrolling "end to end", meaning that it will start nested scrolling, * participate in nested scrolling, and then end nested scrolling all within one call. - * - * @param x The amount of horizontal scroll requested. - * @param y The amount of vertical scroll requested. + * @param x The amount of horizontal scroll requested. + * @param y The amount of vertical scroll requested. * @param motionEvent The originating MotionEvent if any. - * @param type The type of nested scrolling to engage in (TYPE_TOUCH or TYPE_NON_TOUCH). + * @param type The type of nested scrolling to engage in (TYPE_TOUCH or TYPE_NON_TOUCH). */ @SuppressWarnings("SameParameterValue") private void nestedScrollByInternal(int x, int y, @Nullable MotionEvent motionEvent, int type) { @@ -1924,8 +1936,8 @@ private void nestedScrollByInternal(int x, int y, @Nullable MotionEvent motionEv } mReusableIntPair[0] = 0; mReusableIntPair[1] = 0; - boolean canScrollHorizontal = mLayout.canScrollHorizontally(); - boolean canScrollVertical = mLayout.canScrollVertically(); + final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); + final boolean canScrollVertical = mLayout.canScrollVertically(); int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; if (canScrollHorizontal) { @@ -1964,10 +1976,10 @@ private void nestedScrollByInternal(int x, int y, @Nullable MotionEvent motionEv * Scrolls the RV by 'dx' and 'dy' via calls to * {@link LayoutManager#scrollHorizontallyBy(int, Recycler, State)} and * {@link LayoutManager#scrollVerticallyBy(int, Recycler, State)}. - *

    + * * Also sets how much of the scroll was actually consumed in 'consumed' parameter (indexes 0 and * 1 for the x axis and y axis, respectively). - *

    + * * This method should only be called in the context of an existing scroll operation such that * any other necessary operations (such as a call to {@link #consumePendingUpdateOperations()}) * is already handled. @@ -2050,9 +2062,9 @@ void consumePendingUpdateOperations() { * @return True if an existing view holder needs to be updated */ private boolean hasUpdatedView() { - int childCount = mChildHelper.getChildCount(); + final int childCount = mChildHelper.getChildCount(); for (int i = 0; i < childCount; i++) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); if (holder == null || holder.shouldIgnore()) { continue; } @@ -2068,9 +2080,9 @@ private boolean hasUpdatedView() { *

    * It also reports any unused scroll request to the related EdgeEffect. * - * @param x The amount of horizontal scroll request - * @param y The amount of vertical scroll request - * @param ev The originating MotionEvent, or null if not from a touch event. + * @param x The amount of horizontal scroll request + * @param y The amount of vertical scroll request + * @param ev The originating MotionEvent, or null if not from a touch event. * @param type NestedScrollType, TOUCH or NON_TOUCH. * @return Whether any scroll was consumed in either direction. */ @@ -2111,6 +2123,12 @@ boolean scrollByInternal(int x, int y, MotionEvent ev, int type) { if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) { pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY); + // For rotary encoders, we release stretch EdgeEffects after they are pulled, to + // avoid the effects being stuck pulled. + if (Build.VERSION.SDK_INT >= 31 + && MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_ROTARY_ENCODER)) { + releaseGlows(); + } } considerReleasingGlowsOnScroll(x, y); } @@ -2128,8 +2146,8 @@ boolean scrollByInternal(int x, int y, MotionEvent ev, int type) { * deltaX on the edge glow. * * @param deltaX The pointer motion, in pixels, in the horizontal direction, positive - * for moving down and negative for moving up. - * @param y The vertical position of the pointer. + * for moving down and negative for moving up. + * @param y The vertical position of the pointer. * @return The amount of deltaX that has been consumed by the * edge glow. */ @@ -2168,8 +2186,8 @@ private int releaseHorizontalGlow(int deltaX, float y) { * deltaY on the edge glow. * * @param deltaY The pointer motion, in pixels, in the vertical direction, positive - * for moving down and negative for moving up. - * @param x The vertical position of the pointer. + * for moving down and negative for moving up. + * @param x The vertical position of the pointer. * @return The amount of deltaY that has been consumed by the * edge glow. */ @@ -2205,7 +2223,7 @@ private int releaseVerticalGlow(int deltaY, float x) { /** *

    Compute the horizontal offset of the horizontal scrollbar's thumb within the horizontal - * range. This value is used to compute the length of the thumb within the scrollbar's track. + * range. This value is used to compute the position of the thumb within the scrollbar's track. *

    * *

    The range is expressed in arbitrary units that must be the same as the units used by @@ -2266,7 +2284,7 @@ public int computeHorizontalScrollExtent() { * {@link RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)} in your * LayoutManager.

    * - * @return The total horizontal range represented by the vertical scrollbar + * @return The total horizontal range represented by the horizontal scrollbar * @see RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State) */ @Override @@ -2279,7 +2297,7 @@ public int computeHorizontalScrollRange() { /** *

    Compute the vertical offset of the vertical scrollbar's thumb within the vertical range. - * This value is used to compute the length of the thumb within the scrollbar's track.

    + * This value is used to compute the position of the thumb within the scrollbar's track.

    * *

    The range is expressed in arbitrary units that must be the same as the units used by * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}.

    @@ -2379,6 +2397,7 @@ void startInterceptRequestLayout() { */ void stopInterceptRequestLayout(boolean performLayoutChildren) { if (mInterceptRequestLayoutDepth < 1) { + //noinspection PointlessBooleanExpression if (DEBUG) { throw new IllegalStateException("stopInterceptRequestLayout was called more " + "times than startInterceptRequestLayout." @@ -2446,7 +2465,7 @@ public final void suppressLayout(boolean suppress) { } mLayoutWasDefered = false; } else { - long now = SystemClock.uptimeMillis(); + final long now = SystemClock.uptimeMillis(); MotionEvent cancelEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); onTouchEvent(cancelEvent); @@ -2468,15 +2487,6 @@ public final boolean isLayoutSuppressed() { return mLayoutSuppressed; } - /** - * @return true if layout and scroll are frozen - * @deprecated Use {@link #isLayoutSuppressed()}. - */ - @Deprecated - public boolean isLayoutFrozen() { - return isLayoutSuppressed(); - } - /** * Enable or disable layout and scroll. After setLayoutFrozen(true) is called, * Layout requests will be postponed until setLayoutFrozen(false) is called; @@ -2505,6 +2515,15 @@ public void setLayoutFrozen(boolean frozen) { suppressLayout(frozen); } + /** + * @return true if layout and scroll are frozen + * @deprecated Use {@link #isLayoutSuppressed()}. + */ + @Deprecated + public boolean isLayoutFrozen() { + return isLayoutSuppressed(); + } + /** * @deprecated Use {@link #setItemAnimator(ItemAnimator)} ()}. */ @@ -2577,7 +2596,7 @@ public void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interp * {@link #scrollBy(int, int)}. */ public void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator, - int duration) { + int duration) { smoothScrollBy(dx, dy, interpolator, duration, false); } @@ -2615,7 +2634,7 @@ public void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interp */ // Should be considered private. Not private to avoid synthetic accessor. void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator, - int duration, boolean withNestedScrolling) { + int duration, boolean withNestedScrolling) { if (mLayout == null) { Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. " + "Call setLayoutManager with a non-null argument."); @@ -2672,8 +2691,8 @@ public boolean fling(int velocityX, int velocityY) { return false; } - boolean canScrollHorizontal = mLayout.canScrollHorizontally(); - boolean canScrollVertical = mLayout.canScrollVertically(); + final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); + final boolean canScrollVertical = mLayout.canScrollVertically(); if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) { velocityX = 0; @@ -2734,7 +2753,7 @@ public boolean fling(int velocityX, int velocityY) { } if (!dispatchNestedPreFling(velocityX, velocityY)) { - boolean canScroll = canScrollHorizontal || canScrollVertical; + final boolean canScroll = canScrollHorizontal || canScrollVertical; dispatchNestedFling(velocityX, velocityY, canScroll); if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) { @@ -2766,9 +2785,9 @@ public boolean fling(int velocityX, int velocityY) { * EdgeEffect through its normal operation. * * @param edgeEffect The EdgeEffect that might absorb the velocity. - * @param velocity The velocity of the fling motion - * @param size The width or height of the RecyclerView, depending on the edge that the - * EdgeEffect is on. + * @param velocity The velocity of the fling motion + * @param size The width or height of the RecyclerView, depending on the edge that the + * EdgeEffect is on. * @return true if the velocity should be absorbed or false if it should be flung. */ private boolean shouldAbsorb(@NonNull EdgeEffect edgeEffect, int velocity, int size) { @@ -2810,12 +2829,11 @@ int consumeFlingInVerticalStretch(int unconsumedY) { /** * Used by consumeFlingInHorizontalStretch() and consumeFlinInVerticalStretch() for * consuming deltas from EdgeEffects - * * @param unconsumed The unconsumed delta that the EdgeEffets may consume - * @param startGlow The start (top or left) EdgeEffect - * @param endGlow The end (bottom or right) EdgeEffect - * @param size The width or height of the container, depending on whether this is for - * horizontal or vertical EdgeEffects + * @param startGlow The start (top or left) EdgeEffect + * @param endGlow The end (bottom or right) EdgeEffect + * @param size The width or height of the container, depending on whether this is for + * horizontal or vertical EdgeEffects * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume. */ private int consumeFlingInStretch( @@ -2873,6 +2891,7 @@ public int getMinFlingVelocity() { return mMinFlingVelocity; } + /** * Returns the maximum fling velocity used by this RecyclerView. * @@ -3047,18 +3066,6 @@ void invalidateGlows() { mLeftGlow = mRightGlow = mTopGlow = mBottomGlow = null; } - /** - * Retrieves the previously set {@link EdgeEffectFactory} or the default factory if nothing - * was set. - * - * @return The previously set {@link EdgeEffectFactory} - * @see #setEdgeEffectFactory(EdgeEffectFactory) - */ - @NonNull - public EdgeEffectFactory getEdgeEffectFactory() { - return mEdgeEffectFactory; - } - /** * Set a {@link EdgeEffectFactory} for this {@link RecyclerView}. *

    @@ -3074,6 +3081,18 @@ public void setEdgeEffectFactory(@NonNull EdgeEffectFactory edgeEffectFactory) { invalidateGlows(); } + /** + * Retrieves the previously set {@link EdgeEffectFactory} or the default factory if nothing + * was set. + * + * @return The previously set {@link EdgeEffectFactory} + * @see #setEdgeEffectFactory(EdgeEffectFactory) + */ + @NonNull + public EdgeEffectFactory getEdgeEffectFactory() { + return mEdgeEffectFactory; + } + /** * Since RecyclerView is a collection ViewGroup that includes virtual children (items that are * in the Adapter but not visible in the UI), it employs a more involved focus search strategy @@ -3110,19 +3129,19 @@ public View focusSearch(View focused, int direction) { if (result != null) { return result; } - boolean canRunFocusFailure = mAdapter != null && mLayout != null + final boolean canRunFocusFailure = mAdapter != null && mLayout != null && !isComputingLayout() && !mLayoutSuppressed; - FocusFinder ff = FocusFinder.getInstance(); + final FocusFinder ff = FocusFinder.getInstance(); if (canRunFocusFailure && (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD)) { // convert direction to absolute direction and see if we have a view there and if not // tell LayoutManager to add if it can. boolean needsFocusFailureLayout = false; if (mLayout.canScrollVertically()) { - int absDir = + final int absDir = direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP; - View found = ff.findNextFocus(this, focused, absDir); + final View found = ff.findNextFocus(this, focused, absDir); needsFocusFailureLayout = found == null; if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) { // Workaround for broken FOCUS_BACKWARD in API 15 and older devices. @@ -3131,9 +3150,9 @@ public View focusSearch(View focused, int direction) { } if (!needsFocusFailureLayout && mLayout.canScrollHorizontally()) { boolean rtl = mLayout.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL; - int absDir = (direction == View.FOCUS_FORWARD) ^ rtl + final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT; - View found = ff.findNextFocus(this, focused, absDir); + final View found = ff.findNextFocus(this, focused, absDir); needsFocusFailureLayout = found == null; if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) { // Workaround for broken FOCUS_BACKWARD in API 15 and older devices. @@ -3142,7 +3161,7 @@ public View focusSearch(View focused, int direction) { } if (needsFocusFailureLayout) { consumePendingUpdateOperations(); - View focusedItemView = findContainingItemView(focused); + final View focusedItemView = findContainingItemView(focused); if (focusedItemView == null) { // panic, focused view is not a child anymore, cannot call super. return null; @@ -3156,7 +3175,7 @@ public View focusSearch(View focused, int direction) { result = ff.findNextFocus(this, focused, direction); if (result == null && canRunFocusFailure) { consumePendingUpdateOperations(); - View focusedItemView = findContainingItemView(focused); + final View focusedItemView = findContainingItemView(focused); if (focusedItemView == null) { // panic, focused view is not a child anymore, cannot call super. return null; @@ -3211,7 +3230,7 @@ private boolean isPreferredNextFocus(View focused, View next, int direction) { mTempRect2.set(0, 0, next.getWidth(), next.getHeight()); offsetDescendantRectToMyCoords(focused, mTempRect); offsetDescendantRectToMyCoords(next, mTempRect2); - int rtl = mLayout.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL ? -1 : 1; + final int rtl = mLayout.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL ? -1 : 1; int rightness = 0; if ((mTempRect.left < mTempRect2.left || mTempRect.right <= mTempRect2.left) @@ -3274,12 +3293,12 @@ private void requestChildOnScreen(@NonNull View child, @Nullable View focused) { // get item decor offsets w/o refreshing. If they are invalid, there will be another // layout pass to fix them, then it is LayoutManager's responsibility to keep focused // View in viewport. - ViewGroup.LayoutParams focusedLayoutParams = rectView.getLayoutParams(); + final ViewGroup.LayoutParams focusedLayoutParams = rectView.getLayoutParams(); if (focusedLayoutParams instanceof LayoutParams) { // if focused child has item decors, use them. Otherwise, ignore. - LayoutParams lp = (LayoutParams) focusedLayoutParams; + final LayoutParams lp = (LayoutParams) focusedLayoutParams; if (!lp.mInsetsDirty) { - Rect insets = lp.mDecorInsets; + final Rect insets = lp.mDecorInsets; mTempRect.left -= insets.left; mTempRect.right += insets.right; mTempRect.top -= insets.top; @@ -3491,7 +3510,7 @@ private boolean dispatchToOnItemTouchListeners(MotionEvent e) { return findInterceptingOnItemTouchListener(e); } else { mInterceptingOnItemTouchListener.onTouchEvent(this, e); - int action = e.getAction(); + final int action = e.getAction(); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mInterceptingOnItemTouchListener = null; } @@ -3514,9 +3533,9 @@ private boolean dispatchToOnItemTouchListeners(MotionEvent e) { */ private boolean findInterceptingOnItemTouchListener(MotionEvent e) { int action = e.getAction(); - int listenerCount = mOnItemTouchListeners.size(); + final int listenerCount = mOnItemTouchListeners.size(); for (int i = 0; i < listenerCount; i++) { - OnItemTouchListener listener = mOnItemTouchListeners.get(i); + final OnItemTouchListener listener = mOnItemTouchListeners.get(i); if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) { mInterceptingOnItemTouchListener = listener; return true; @@ -3545,16 +3564,16 @@ public boolean onInterceptTouchEvent(MotionEvent e) { return false; } - boolean canScrollHorizontally = mLayout.canScrollHorizontally(); - boolean canScrollVertically = mLayout.canScrollVertically(); + final boolean canScrollHorizontally = mLayout.canScrollHorizontally(); + final boolean canScrollVertically = mLayout.canScrollVertically(); if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(e); - int action = e.getActionMasked(); - int actionIndex = e.getActionIndex(); + final int action = e.getActionMasked(); + final int actionIndex = e.getActionIndex(); switch (action) { case MotionEvent.ACTION_DOWN: @@ -3591,18 +3610,18 @@ public boolean onInterceptTouchEvent(MotionEvent e) { break; case MotionEvent.ACTION_MOVE: { - int index = e.findPointerIndex(mScrollPointerId); + final int index = e.findPointerIndex(mScrollPointerId); if (index < 0) { Log.e(TAG, "Error processing scroll; pointer index for id " + mScrollPointerId + " not found. Did any MotionEvents get skipped?"); return false; } - int x = (int) (e.getX(index) + 0.5f); - int y = (int) (e.getY(index) + 0.5f); + final int x = (int) (e.getX(index) + 0.5f); + final int y = (int) (e.getY(index) + 0.5f); if (mScrollState != SCROLL_STATE_DRAGGING) { - int dx = x - mInitialTouchX; - int dy = y - mInitialTouchY; + final int dx = x - mInitialTouchX; + final int dy = y - mInitialTouchY; boolean startScroll = false; if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) { mLastTouchX = x; @@ -3675,15 +3694,14 @@ private boolean stopGlowAnimations(MotionEvent e) { @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { - int listenerCount = mOnItemTouchListeners.size(); + final int listenerCount = mOnItemTouchListeners.size(); for (int i = 0; i < listenerCount; i++) { - OnItemTouchListener listener = mOnItemTouchListeners.get(i); + final OnItemTouchListener listener = mOnItemTouchListeners.get(i); listener.onRequestDisallowInterceptTouchEvent(disallowIntercept); } super.requestDisallowInterceptTouchEvent(disallowIntercept); } - @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent e) { if (mLayoutSuppressed || mIgnoreMotionEventTillDown) { @@ -3698,21 +3716,21 @@ public boolean onTouchEvent(MotionEvent e) { return false; } - boolean canScrollHorizontally = mLayout.canScrollHorizontally(); - boolean canScrollVertically = mLayout.canScrollVertically(); + final boolean canScrollHorizontally = mLayout.canScrollHorizontally(); + final boolean canScrollVertically = mLayout.canScrollVertically(); if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } boolean eventAddedToVelocityTracker = false; - int action = e.getActionMasked(); - int actionIndex = e.getActionIndex(); + final int action = e.getActionMasked(); + final int actionIndex = e.getActionIndex(); if (action == MotionEvent.ACTION_DOWN) { mNestedOffsets[0] = mNestedOffsets[1] = 0; } - MotionEvent vtev = MotionEvent.obtain(e); + final MotionEvent vtev = MotionEvent.obtain(e); vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]); switch (action) { @@ -3740,15 +3758,15 @@ public boolean onTouchEvent(MotionEvent e) { break; case MotionEvent.ACTION_MOVE: { - int index = e.findPointerIndex(mScrollPointerId); + final int index = e.findPointerIndex(mScrollPointerId); if (index < 0) { Log.e(TAG, "Error processing scroll; pointer index for id " + mScrollPointerId + " not found. Did any MotionEvents get skipped?"); return false; } - int x = (int) (e.getX(index) + 0.5f); - int y = (int) (e.getY(index) + 0.5f); + final int x = (int) (e.getX(index) + 0.5f); + final int y = (int) (e.getY(index) + 0.5f); int dx = mLastTouchX - x; int dy = mLastTouchY - y; @@ -3824,9 +3842,9 @@ public boolean onTouchEvent(MotionEvent e) { mVelocityTracker.addMovement(vtev); eventAddedToVelocityTracker = true; mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); - float xvel = canScrollHorizontally + final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0; - float yvel = canScrollVertically + final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0; if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { setScrollState(SCROLL_STATE_IDLE); @@ -3863,10 +3881,10 @@ private void cancelScroll() { } private void onPointerUp(MotionEvent e) { - int actionIndex = e.getActionIndex(); + final int actionIndex = e.getActionIndex(); if (e.getPointerId(actionIndex) == mScrollPointerId) { // Pick a new pointer to pick up the slack. - int newIndex = actionIndex == 0 ? 1 : 0; + final int newIndex = actionIndex == 0 ? 1 : 0; mScrollPointerId = e.getPointerId(newIndex); mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f); mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f); @@ -3882,7 +3900,7 @@ public boolean onGenericMotionEvent(MotionEvent event) { return false; } if (event.getAction() == MotionEvent.ACTION_SCROLL) { - float vScroll, hScroll; + final float vScroll, hScroll; if ((event.getSource() & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { if (mLayout.canScrollVertically()) { // Inverse the sign of the vertical scroll to align the scroll orientation @@ -3897,7 +3915,7 @@ public boolean onGenericMotionEvent(MotionEvent event) { hScroll = 0f; } } else if ((event.getSource() & InputDeviceCompat.SOURCE_ROTARY_ENCODER) != 0) { - float axisScroll = event.getAxisValue(MotionEventCompat.AXIS_SCROLL); + final float axisScroll = event.getAxisValue(MotionEventCompat.AXIS_SCROLL); if (mLayout.canScrollVertically()) { // Invert the sign of the vertical scroll to align the scroll orientation // with AbsListView. @@ -3930,8 +3948,8 @@ protected void onMeasure(int widthSpec, int heightSpec) { return; } if (mLayout.isAutoMeasureEnabled()) { - int widthMode = MeasureSpec.getMode(widthSpec); - int heightMode = MeasureSpec.getMode(heightSpec); + final int widthMode = MeasureSpec.getMode(widthSpec); + final int heightMode = MeasureSpec.getMode(heightSpec); /** * This specific call should be considered deprecated and replaced with @@ -4026,10 +4044,10 @@ protected void onMeasure(int widthSpec, int heightSpec) { void defaultOnMeasure(int widthSpec, int heightSpec) { // calling LayoutManager here is not pretty but that API is already public and it is better // than creating another method since this is internal. - int width = LayoutManager.chooseSize(widthSpec, + final int width = LayoutManager.chooseSize(widthSpec, getPaddingLeft() + getPaddingRight(), ViewCompat.getMinimumWidth(this)); - int height = LayoutManager.chooseSize(heightSpec, + final int height = LayoutManager.chooseSize(heightSpec, getPaddingTop() + getPaddingBottom(), ViewCompat.getMinimumHeight(this)); @@ -4045,6 +4063,28 @@ protected void onSizeChanged(int w, int h, int oldw, int oldh) { } } + /** + * Sets the {@link ItemAnimator} that will handle animations involving changes + * to the items in this RecyclerView. By default, RecyclerView instantiates and + * uses an instance of {@link DefaultItemAnimator}. Whether item animations are + * enabled for the RecyclerView depends on the ItemAnimator and whether + * the LayoutManager {@link LayoutManager#supportsPredictiveItemAnimations() + * supports item animations}. + * + * @param animator The ItemAnimator being set. If null, no animations will occur + * when changes occur to the items in this RecyclerView. + */ + public void setItemAnimator(@Nullable ItemAnimator animator) { + if (mItemAnimator != null) { + mItemAnimator.endAnimations(); + mItemAnimator.setListener(null); + } + mItemAnimator = animator; + if (mItemAnimator != null) { + mItemAnimator.setListener(mItemAnimatorListener); + } + } + void onEnterLayoutOrScroll() { mLayoutOrScrollCounter++; } @@ -4074,10 +4114,10 @@ boolean isAccessibilityEnabled() { @SuppressWarnings("deprecation") private void dispatchContentChangedIfNecessary() { - int flags = mEatenAccessibilityChangeFlags; + final int flags = mEatenAccessibilityChangeFlags; mEatenAccessibilityChangeFlags = 0; if (flags != 0 && isAccessibilityEnabled()) { - AccessibilityEvent event; + final AccessibilityEvent event; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { event = new AccessibilityEvent(); } else { @@ -4163,28 +4203,6 @@ public ItemAnimator getItemAnimator() { return mItemAnimator; } - /** - * Sets the {@link ItemAnimator} that will handle animations involving changes - * to the items in this RecyclerView. By default, RecyclerView instantiates and - * uses an instance of {@link DefaultItemAnimator}. Whether item animations are - * enabled for the RecyclerView depends on the ItemAnimator and whether - * the LayoutManager {@link LayoutManager#supportsPredictiveItemAnimations() - * supports item animations}. - * - * @param animator The ItemAnimator being set. If null, no animations will occur - * when changes occur to the items in this RecyclerView. - */ - public void setItemAnimator(@Nullable ItemAnimator animator) { - if (mItemAnimator != null) { - mItemAnimator.endAnimations(); - mItemAnimator.setListener(null); - } - mItemAnimator = animator; - if (mItemAnimator != null) { - mItemAnimator.setListener(mItemAnimatorListener); - } - } - /** * Post a runnable to the next frame to run pending item animations. Only the first such * request will be posted, governed by the mPostedAnimatorRunner flag. @@ -4280,8 +4298,8 @@ void dispatchLayout() { // dimensions of the RV are not equal to the last measured dimensions of RV, we need to // measure and layout children one last time. boolean needsRemeasureDueToExactSkip = mLastAutoMeasureSkippedDueToExact - && (mLastAutoMeasureNonExactMeasuredWidth != getWidth() - || mLastAutoMeasureNonExactMeasuredHeight != getHeight()); + && (mLastAutoMeasureNonExactMeasuredWidth != getWidth() + || mLastAutoMeasureNonExactMeasuredHeight != getHeight()); mLastAutoMeasureNonExactMeasuredWidth = 0; mLastAutoMeasureNonExactMeasuredHeight = 0; mLastAutoMeasureSkippedDueToExact = false; @@ -4317,7 +4335,7 @@ private void saveFocusInfo() { child = getFocusedChild(); } - ViewHolder focusedVh = child == null ? null : findContainingViewHolder(child); + final ViewHolder focusedVh = child == null ? null : findContainingViewHolder(child); if (focusedVh == null) { resetFocusInfo(); } else { @@ -4327,7 +4345,7 @@ private void saveFocusInfo() { // removed item. mState.mFocusedItemPosition = mDataSetHasChangedAfterLayout ? NO_POSITION : (focusedVh.isRemoved() ? focusedVh.mOldPosition - : focusedVh.getAbsoluteAdapterPosition()); + : focusedVh.getAbsoluteAdapterPosition()); mState.mFocusedSubChildId = getDeepestFocusedViewWithId(focusedVh.itemView); } } @@ -4352,7 +4370,7 @@ private View findNextViewToFocus() { int startFocusSearchIndex = mState.mFocusedItemPosition != -1 ? mState.mFocusedItemPosition : 0; ViewHolder nextFocus; - int itemCount = mState.getItemCount(); + final int itemCount = mState.getItemCount(); for (int i = startFocusSearchIndex; i < itemCount; i++) { nextFocus = findViewHolderForAdapterPosition(i); if (nextFocus == null) { @@ -4362,7 +4380,7 @@ private View findNextViewToFocus() { return nextFocus.itemView; } } - int limit = Math.min(itemCount, startFocusSearchIndex); + final int limit = Math.min(itemCount, startFocusSearchIndex); for (int i = limit - 1; i >= 0; i--) { nextFocus = findViewHolderForAdapterPosition(i); if (nextFocus == null) { @@ -4387,7 +4405,7 @@ private void recoverFocusFromState() { } // only recover focus if RV itself has the focus or the focused view is hidden if (!isFocused()) { - View focusedChild = getFocusedChild(); + final View focusedChild = getFocusedChild(); if (IGNORE_DETACHED_FOCUSED_CHILD && (focusedChild.getParent() == null || !focusedChild.hasFocus())) { // Special handling of API 15-. A focused child can be invalid because mFocus is not @@ -4453,7 +4471,7 @@ private int getDeepestFocusedViewWithId(View view) { int lastKnownId = view.getId(); while (!view.isFocused() && view instanceof ViewGroup && view.hasFocus()) { view = ((ViewGroup) view).getFocusedChild(); - int id = view.getId(); + final int id = view.getId(); if (id != View.NO_ID) { lastKnownId = view.getId(); } @@ -4463,7 +4481,7 @@ private int getDeepestFocusedViewWithId(View view) { final void fillRemainingScrollValues(State state) { if (getScrollState() == SCROLL_STATE_SETTLING) { - OverScroller scroller = mViewFlinger.mOverScroller; + final OverScroller scroller = mViewFlinger.mOverScroller; state.mRemainingScrollHorizontal = scroller.getFinalX() - scroller.getCurrX(); state.mRemainingScrollVertical = scroller.getFinalY() - scroller.getCurrY(); } else { @@ -4498,11 +4516,11 @@ private void dispatchLayoutStep1() { // Step 0: Find out where all non-removed items are, pre-layout int count = mChildHelper.getChildCount(); for (int i = 0; i < count; ++i) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) { continue; } - ItemHolderInfo animationInfo = mItemAnimator + final ItemHolderInfo animationInfo = mItemAnimator .recordPreLayoutInformation(mState, holder, ItemAnimator.buildAdapterChangeFlagsForAnimations(holder), holder.getUnmodifiedPayloads()); @@ -4529,15 +4547,15 @@ private void dispatchLayoutStep1() { // Save old positions so that LayoutManager can run its mapping logic. saveOldPositions(); - boolean didStructureChange = mState.mStructureChanged; + final boolean didStructureChange = mState.mStructureChanged; mState.mStructureChanged = false; // temporarily disable flag because we are asking for previous layout mLayout.onLayoutChildren(mRecycler, mState); mState.mStructureChanged = didStructureChange; for (int i = 0; i < mChildHelper.getChildCount(); ++i) { - View child = mChildHelper.getChildAt(i); - ViewHolder viewHolder = getChildViewHolderInt(child); + final View child = mChildHelper.getChildAt(i); + final ViewHolder viewHolder = getChildViewHolderInt(child); if (viewHolder.shouldIgnore()) { continue; } @@ -4548,7 +4566,7 @@ private void dispatchLayoutStep1() { if (!wasHidden) { flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; } - ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation( + final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation( mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads()); if (wasHidden) { recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo); @@ -4558,8 +4576,10 @@ private void dispatchLayoutStep1() { } } // we don't process disappearing list because they may re-appear in post layout pass. + clearOldPositions(); + } else { + clearOldPositions(); } - clearOldPositions(); onExitLayoutOrScroll(); stopInterceptRequestLayout(false); mState.mLayoutStep = State.STEP_LAYOUT; @@ -4614,7 +4634,7 @@ private void dispatchLayoutStep3() { continue; } long key = getChangedHolderKey(holder); - ItemHolderInfo animationInfo = mItemAnimator + final ItemHolderInfo animationInfo = mItemAnimator .recordPostLayoutInformation(mState, holder); ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key); if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) { @@ -4628,14 +4648,14 @@ private void dispatchLayoutStep3() { // On the other hand, if it is the same view holder instance, we run a // disappearing animation instead because we are not going to rebind the updated // VH unless it is enforced by the layout manager. - boolean oldDisappearing = mViewInfoStore.isDisappearing( + final boolean oldDisappearing = mViewInfoStore.isDisappearing( oldChangeViewHolder); - boolean newDisappearing = mViewInfoStore.isDisappearing(holder); + final boolean newDisappearing = mViewInfoStore.isDisappearing(holder); if (oldDisappearing && oldChangeViewHolder == holder) { // run disappear animation instead of change mViewInfoStore.addToPostLayout(holder, animationInfo); } else { - ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout( + final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout( oldChangeViewHolder); // we add and remove so that any post info is merged. mViewInfoStore.addToPostLayout(holder, animationInfo); @@ -4694,7 +4714,7 @@ private void dispatchLayoutStep3() { *

    * If it is not an expected error, we at least print an error to notify the developer and ignore * the animation. - *

    + * * https://code.google.com/p/android/issues/detail?id=193958 * * @param key The change key @@ -4702,16 +4722,16 @@ private void dispatchLayoutStep3() { * @param oldChangeViewHolder Changed ViewHolder */ private void handleMissingPreInfoForChangeError(long key, - ViewHolder holder, ViewHolder oldChangeViewHolder) { + ViewHolder holder, ViewHolder oldChangeViewHolder) { // check if two VH have the same key, if so, print that as an error - int childCount = mChildHelper.getChildCount(); + final int childCount = mChildHelper.getChildCount(); for (int i = 0; i < childCount; i++) { View view = mChildHelper.getChildAt(i); ViewHolder other = getChildViewHolderInt(view); if (other == holder) { continue; } - long otherKey = getChangedHolderKey(other); + final long otherKey = getChangedHolderKey(other); if (otherKey == key) { if (mAdapter != null && mAdapter.hasStableIds()) { throw new IllegalStateException("Two different ViewHolders have the same stable" @@ -4738,7 +4758,7 @@ private void handleMissingPreInfoForChangeError(long key, * also clears the bounce back flag. */ void recordAnimationInfoIfBouncedHiddenView(ViewHolder viewHolder, - ItemHolderInfo animationInfo) { + ItemHolderInfo animationInfo) { // looks like this view bounced back from hidden list! viewHolder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); if (mState.mTrackOldChangeHolders && viewHolder.isUpdated() @@ -4750,7 +4770,7 @@ void recordAnimationInfoIfBouncedHiddenView(ViewHolder viewHolder, } private void findMinMaxChildLayoutPositions(int[] into) { - int count = mChildHelper.getChildCount(); + final int count = mChildHelper.getChildCount(); if (count == 0) { into[0] = NO_POSITION; into[1] = NO_POSITION; @@ -4759,11 +4779,11 @@ private void findMinMaxChildLayoutPositions(int[] into) { int minPositionPreLayout = Integer.MAX_VALUE; int maxPositionPreLayout = Integer.MIN_VALUE; for (int i = 0; i < count; ++i) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); if (holder.shouldIgnore()) { continue; } - int pos = holder.getLayoutPosition(); + final int pos = holder.getLayoutPosition(); if (pos < minPositionPreLayout) { minPositionPreLayout = pos; } @@ -4811,7 +4831,7 @@ long getChangedHolderKey(ViewHolder holder) { } void animateAppearance(@NonNull ViewHolder itemHolder, - @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { + @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { itemHolder.setIsRecyclable(false); if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) { postAnimationRunner(); @@ -4819,7 +4839,7 @@ void animateAppearance(@NonNull ViewHolder itemHolder, } void animateDisappearance(@NonNull ViewHolder holder, - @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { + @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { addAnimatingView(holder); holder.setIsRecyclable(false); if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) { @@ -4828,8 +4848,8 @@ void animateDisappearance(@NonNull ViewHolder holder, } private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder, - @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo, - boolean oldHolderDisappearing, boolean newHolderDisappearing) { + @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo, + boolean oldHolderDisappearing, boolean newHolderDisappearing) { oldHolder.setIsRecyclable(false); if (oldHolderDisappearing) { addAnimatingView(oldHolder); @@ -4868,19 +4888,19 @@ public void requestLayout() { } void markItemDecorInsetsDirty() { - int childCount = mChildHelper.getUnfilteredChildCount(); + final int childCount = mChildHelper.getUnfilteredChildCount(); for (int i = 0; i < childCount; i++) { - View child = mChildHelper.getUnfilteredChildAt(i); + final View child = mChildHelper.getUnfilteredChildAt(i); ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true; } mRecycler.markItemDecorInsetsDirty(); } @Override - public void draw(Canvas c) { + public void draw(@NonNull Canvas c) { super.draw(c); - int count = mItemDecorations.size(); + final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDrawOver(c, this, mState); } @@ -4888,15 +4908,15 @@ public void draw(Canvas c) { // need find children closest to edges. Not sure if it is worth the effort. boolean needsInvalidate = false; if (mLeftGlow != null && !mLeftGlow.isFinished()) { - int restore = c.save(); - int padding = mClipToPadding ? getPaddingBottom() : 0; + final int restore = c.save(); + final int padding = mClipToPadding ? getPaddingBottom() : 0; c.rotate(270); c.translate(-getHeight() + padding, 0); needsInvalidate = mLeftGlow != null && mLeftGlow.draw(c); c.restoreToCount(restore); } if (mTopGlow != null && !mTopGlow.isFinished()) { - int restore = c.save(); + final int restore = c.save(); if (mClipToPadding) { c.translate(getPaddingLeft(), getPaddingTop()); } @@ -4904,16 +4924,16 @@ public void draw(Canvas c) { c.restoreToCount(restore); } if (mRightGlow != null && !mRightGlow.isFinished()) { - int restore = c.save(); - int width = getWidth(); - int padding = mClipToPadding ? getPaddingTop() : 0; + final int restore = c.save(); + final int width = getWidth(); + final int padding = mClipToPadding ? getPaddingTop() : 0; c.rotate(90); c.translate(padding, -width); needsInvalidate |= mRightGlow != null && mRightGlow.draw(c); c.restoreToCount(restore); } if (mBottomGlow != null && !mBottomGlow.isFinished()) { - int restore = c.save(); + final int restore = c.save(); c.rotate(180); if (mClipToPadding) { c.translate(-getWidth() + getPaddingRight(), -getHeight() + getPaddingBottom()); @@ -4938,10 +4958,10 @@ public void draw(Canvas c) { } @Override - public void onDraw(Canvas c) { + public void onDraw(@NonNull Canvas c) { super.onDraw(c); - int count = mItemDecorations.size(); + final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDraw(c, this, mState); } @@ -4989,9 +5009,9 @@ public boolean isAnimating() { } void saveOldPositions() { - int childCount = mChildHelper.getUnfilteredChildCount(); + final int childCount = mChildHelper.getUnfilteredChildCount(); for (int i = 0; i < childCount; i++) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); if (DEBUG && holder.mPosition == -1 && !holder.isRemoved()) { throw new IllegalStateException("view holder cannot have position -1 unless it" + " is removed" + exceptionLabel()); @@ -5003,9 +5023,9 @@ void saveOldPositions() { } void clearOldPositions() { - int childCount = mChildHelper.getUnfilteredChildCount(); + final int childCount = mChildHelper.getUnfilteredChildCount(); for (int i = 0; i < childCount; i++) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); if (!holder.shouldIgnore()) { holder.clearOldPosition(); } @@ -5014,8 +5034,8 @@ void clearOldPositions() { } void offsetPositionRecordsForMove(int from, int to) { - int childCount = mChildHelper.getUnfilteredChildCount(); - int start, end, inBetweenOffset; + final int childCount = mChildHelper.getUnfilteredChildCount(); + final int start, end, inBetweenOffset; if (from < to) { start = from; end = to; @@ -5027,7 +5047,7 @@ void offsetPositionRecordsForMove(int from, int to) { } for (int i = 0; i < childCount; i++) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); if (holder == null || holder.mPosition < start || holder.mPosition > end) { continue; } @@ -5048,9 +5068,9 @@ void offsetPositionRecordsForMove(int from, int to) { } void offsetPositionRecordsForInsert(int positionStart, int itemCount) { - int childCount = mChildHelper.getUnfilteredChildCount(); + final int childCount = mChildHelper.getUnfilteredChildCount(); for (int i = 0; i < childCount; i++) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); if (holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart) { if (DEBUG) { Log.d(TAG, "offsetPositionRecordsForInsert attached child " + i + " holder " @@ -5065,11 +5085,11 @@ void offsetPositionRecordsForInsert(int positionStart, int itemCount) { } void offsetPositionRecordsForRemove(int positionStart, int itemCount, - boolean applyToPreLayout) { - int positionEnd = positionStart + itemCount; - int childCount = mChildHelper.getUnfilteredChildCount(); + boolean applyToPreLayout) { + final int positionEnd = positionStart + itemCount; + final int childCount = mChildHelper.getUnfilteredChildCount(); for (int i = 0; i < childCount; i++) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); if (holder != null && !holder.shouldIgnore()) { if (holder.mPosition >= positionEnd) { if (DEBUG) { @@ -5101,12 +5121,12 @@ void offsetPositionRecordsForRemove(int positionStart, int itemCount, * @param itemCount Number of views that must explicitly be rebound */ void viewRangeUpdate(int positionStart, int itemCount, Object payload) { - int childCount = mChildHelper.getUnfilteredChildCount(); - int positionEnd = positionStart + itemCount; + final int childCount = mChildHelper.getUnfilteredChildCount(); + final int positionEnd = positionStart + itemCount; for (int i = 0; i < childCount; i++) { - View child = mChildHelper.getUnfilteredChildAt(i); - ViewHolder holder = getChildViewHolderInt(child); + final View child = mChildHelper.getUnfilteredChildAt(i); + final ViewHolder holder = getChildViewHolderInt(child); if (holder == null || holder.shouldIgnore()) { continue; } @@ -5152,9 +5172,9 @@ void processDataSetCompletelyChanged(boolean dispatchItemsChanged) { * data change event. */ void markKnownViewsInvalid() { - int childCount = mChildHelper.getUnfilteredChildCount(); + final int childCount = mChildHelper.getUnfilteredChildCount(); for (int i = 0; i < childCount; i++) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); if (holder != null && !holder.shouldIgnore()) { holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID); } @@ -5217,7 +5237,7 @@ public void setPreserveFocusAfterLayout(boolean preserveFocusAfterLayout) { * @return The child view's ViewHolder */ public ViewHolder getChildViewHolder(@NonNull View child) { - ViewParent parent = child.getParent(); + final ViewParent parent = child.getParent(); if (parent != null && parent != this) { throw new IllegalArgumentException("View " + child + " is not a direct child of " + this); @@ -5259,6 +5279,14 @@ public ViewHolder findContainingViewHolder(@NonNull View view) { return itemView == null ? null : getChildViewHolder(itemView); } + + static ViewHolder getChildViewHolderInt(View child) { + if (child == null) { + return null; + } + return ((LayoutParams) child.getLayoutParams()).mViewHolder; + } + /** * @deprecated use {@link #getChildAdapterPosition(View)} or * {@link #getChildLayoutPosition(View)}. @@ -5275,7 +5303,7 @@ public int getChildPosition(@NonNull View child) { * @return Adapter position corresponding to the given view or {@link #NO_POSITION} */ public int getChildAdapterPosition(@NonNull View child) { - ViewHolder holder = getChildViewHolderInt(child); + final ViewHolder holder = getChildViewHolderInt(child); return holder != null ? holder.getAbsoluteAdapterPosition() : NO_POSITION; } @@ -5290,7 +5318,7 @@ public int getChildAdapterPosition(@NonNull View child) { * the View is representing a removed item. */ public int getChildLayoutPosition(@NonNull View child) { - ViewHolder holder = getChildViewHolderInt(child); + final ViewHolder holder = getChildViewHolderInt(child); return holder != null ? holder.getLayoutPosition() : NO_POSITION; } @@ -5304,7 +5332,7 @@ public long getChildItemId(@NonNull View child) { if (mAdapter == null || !mAdapter.hasStableIds()) { return NO_ID; } - ViewHolder holder = getChildViewHolderInt(child); + final ViewHolder holder = getChildViewHolderInt(child); return holder != null ? holder.getItemId() : NO_ID; } @@ -5368,11 +5396,11 @@ public ViewHolder findViewHolderForAdapterPosition(int position) { if (mDataSetHasChangedAfterLayout) { return null; } - int childCount = mChildHelper.getUnfilteredChildCount(); + final int childCount = mChildHelper.getUnfilteredChildCount(); // hidden VHs are not preferred but if that is the only one we find, we rather return it ViewHolder hidden = null; for (int i = 0; i < childCount; i++) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); if (holder != null && !holder.isRemoved() && getAdapterPositionInRecyclerView(holder) == position) { if (mChildHelper.isHidden(holder.itemView)) { @@ -5387,10 +5415,10 @@ && getAdapterPositionInRecyclerView(holder) == position) { @Nullable ViewHolder findViewHolderForPosition(int position, boolean checkNewPosition) { - int childCount = mChildHelper.getUnfilteredChildCount(); + final int childCount = mChildHelper.getUnfilteredChildCount(); ViewHolder hidden = null; for (int i = 0; i < childCount; i++) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); if (holder != null && !holder.isRemoved()) { if (checkNewPosition) { if (holder.mPosition != position) { @@ -5419,7 +5447,7 @@ ViewHolder findViewHolderForPosition(int position, boolean checkNewPosition) { *

    * This method checks only the children of RecyclerView. If the item with the given * id is not laid out, it will not create a new one. - *

    + * * When the ItemAnimator is running a change animation, there might be 2 ViewHolders with the * same id. In this case, the updated ViewHolder will be returned. * @@ -5430,10 +5458,10 @@ public ViewHolder findViewHolderForItemId(long id) { if (mAdapter == null || !mAdapter.hasStableIds()) { return null; } - int childCount = mChildHelper.getUnfilteredChildCount(); + final int childCount = mChildHelper.getUnfilteredChildCount(); ViewHolder hidden = null; for (int i = 0; i < childCount; i++) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); if (holder != null && !holder.isRemoved() && holder.getItemId() == id) { if (mChildHelper.isHidden(holder.itemView)) { hidden = holder; @@ -5454,11 +5482,11 @@ public ViewHolder findViewHolderForItemId(long id) { */ @Nullable public View findChildViewUnder(float x, float y) { - int count = mChildHelper.getChildCount(); + final int count = mChildHelper.getChildCount(); for (int i = count - 1; i >= 0; i--) { - View child = mChildHelper.getChildAt(i); - float translationX = child.getTranslationX(); - float translationY = child.getTranslationY(); + final View child = mChildHelper.getChildAt(i); + final float translationX = child.getTranslationX(); + final float translationY = child.getTranslationY(); if (x >= child.getLeft() + translationX && x <= child.getRight() + translationX && y >= child.getTop() + translationY @@ -5470,7 +5498,7 @@ public View findChildViewUnder(float x, float y) { } @Override - public boolean drawChild(Canvas canvas, View child, long drawingTime) { + public boolean drawChild(@NonNull Canvas canvas, View child, long drawingTime) { return super.drawChild(canvas, child, drawingTime); } @@ -5481,7 +5509,7 @@ public boolean drawChild(Canvas canvas, View child, long drawingTime) { * @param dy Vertical pixel offset to apply to the bounds of all child views */ public void offsetChildrenVertical(@Px int dy) { - int childCount = mChildHelper.getChildCount(); + final int childCount = mChildHelper.getChildCount(); for (int i = 0; i < childCount; i++) { mChildHelper.getChildAt(i).offsetTopAndBottom(dy); } @@ -5519,7 +5547,7 @@ public void onChildDetachedFromWindow(@NonNull View child) { * @param dx Horizontal pixel offset to apply to the bounds of all child views */ public void offsetChildrenHorizontal(@Px int dx) { - int childCount = mChildHelper.getChildCount(); + final int childCount = mChildHelper.getChildCount(); for (int i = 0; i < childCount; i++) { mChildHelper.getChildAt(i).offsetLeftAndRight(dx); } @@ -5536,8 +5564,17 @@ public void getDecoratedBoundsWithMargins(@NonNull View view, @NonNull Rect outB getDecoratedBoundsWithMarginsInt(view, outBounds); } + static void getDecoratedBoundsWithMarginsInt(View view, Rect outBounds) { + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + final Rect insets = lp.mDecorInsets; + outBounds.set(view.getLeft() - insets.left - lp.leftMargin, + view.getTop() - insets.top - lp.topMargin, + view.getRight() + insets.right + lp.rightMargin, + view.getBottom() + insets.bottom + lp.bottomMargin); + } + Rect getItemDecorInsetsForChild(View child) { - LayoutParams lp = (LayoutParams) child.getLayoutParams(); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.mInsetsDirty) { return lp.mDecorInsets; } @@ -5546,9 +5583,9 @@ Rect getItemDecorInsetsForChild(View child) { // changed/invalid items should not be updated until they are rebound. return lp.mDecorInsets; } - Rect insets = lp.mDecorInsets; + final Rect insets = lp.mDecorInsets; insets.set(0, 0, 0, 0); - int decorCount = mItemDecorations.size(); + final int decorCount = mItemDecorations.size(); for (int i = 0; i < decorCount; i++) { mTempRect.set(0, 0, 0, 0); mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState); @@ -5591,8 +5628,8 @@ void dispatchOnScrolled(int hresult, int vresult) { // properties occurred. Pass negative hresult and vresult as old values so that // postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt) in onScrollChanged // sends the scrolled accessibility event correctly. - int scrollX = getScrollX(); - int scrollY = getScrollY(); + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); onScrollChanged(scrollX, scrollY, scrollX - hresult, scrollY - vresult); // Pass the real deltas to onScrolled, the RecyclerView-specific method. @@ -5628,14 +5665,13 @@ public void onScrollStateChanged(int state) { /** * Copied from OverScroller, this returns the distance that a fling with the given velocity * will go. - * * @param velocity The velocity of the fling * @return The distance that will be traveled by a fling of the given velocity. */ private float getSplineFlingDistance(int velocity) { - double l = + final double l = Math.log(INFLEXION * Math.abs(velocity) / (SCROLL_FRICTION * mPhysicalCoef)); - double decelMinusOne = DECELERATION_RATE - 1.0; + final double decelMinusOne = DECELERATION_RATE - 1.0; return (float) (SCROLL_FRICTION * mPhysicalCoef * Math.exp(DECELERATION_RATE / decelMinusOne * l)); } @@ -5680,350 +5716,367 @@ public boolean hasPendingAdapterUpdates() { || mAdapterHelper.hasPendingUpdates(); } - void repositionShadowingViews() { - // Fix up shadow views used by change animations - int count = mChildHelper.getChildCount(); - for (int i = 0; i < count; i++) { - View view = mChildHelper.getChildAt(i); - ViewHolder holder = getChildViewHolder(view); - if (holder != null && holder.mShadowingHolder != null) { - View shadowingView = holder.mShadowingHolder.itemView; - int left = view.getLeft(); - int top = view.getTop(); - if (left != shadowingView.getLeft() || top != shadowingView.getTop()) { - shadowingView.layout(left, top, - left + shadowingView.getWidth(), - top + shadowingView.getHeight()); - } - } - } - } + // Effectively private. Set to default to avoid synthetic accessor. + class ViewFlinger implements Runnable { + private int mLastFlingX; + private int mLastFlingY; + OverScroller mOverScroller; + Interpolator mInterpolator = sQuinticInterpolator; - /** - * Time base for deadline-aware work scheduling. Overridable for testing. - *

    - * Will return 0 to avoid cost of System.nanoTime where deadline-aware work scheduling - * isn't relevant. - */ - long getNanoTime() { - if (ALLOW_THREAD_GAP_WORK) { - return System.nanoTime(); - } else { - return 0; - } - } + // When set to true, postOnAnimation callbacks are delayed until the run method completes + private boolean mEatRunOnAnimationRequest = false; - @SuppressWarnings("unchecked") - void dispatchChildDetached(View child) { - ViewHolder viewHolder = getChildViewHolderInt(child); - onChildDetachedFromWindow(child); - if (mAdapter != null && viewHolder != null) { - mAdapter.onViewDetachedFromWindow(viewHolder); - } - if (mOnChildAttachStateListeners != null) { - int cnt = mOnChildAttachStateListeners.size(); - for (int i = cnt - 1; i >= 0; i--) { - mOnChildAttachStateListeners.get(i).onChildViewDetachedFromWindow(child); - } - } - } + // Tracks if postAnimationCallback should be re-attached when it is done + private boolean mReSchedulePostAnimationCallback = false; - @SuppressWarnings("unchecked") - void dispatchChildAttached(View child) { - ViewHolder viewHolder = getChildViewHolderInt(child); - onChildAttachedToWindow(child); - if (mAdapter != null && viewHolder != null) { - mAdapter.onViewAttachedToWindow(viewHolder); + ViewFlinger() { + mOverScroller = new OverScroller(getContext(), sQuinticInterpolator); } - if (mOnChildAttachStateListeners != null) { - int cnt = mOnChildAttachStateListeners.size(); - for (int i = cnt - 1; i >= 0; i--) { - mOnChildAttachStateListeners.get(i).onChildViewAttachedToWindow(child); + + @Override + public void run() { + if (mLayout == null) { + stop(); + return; // no layout, cannot scroll. } - } - } - /** - * This method is here so that we can control the important for a11y changes and test it. - */ - @VisibleForTesting - boolean setChildImportantForAccessibilityInternal(ViewHolder viewHolder, - int importantForAccessibility) { - if (isComputingLayout()) { - viewHolder.mPendingAccessibilityState = importantForAccessibility; - mPendingAccessibilityImportanceChange.add(viewHolder); - return false; - } - ViewCompat.setImportantForAccessibility(viewHolder.itemView, importantForAccessibility); - return true; - } + mReSchedulePostAnimationCallback = false; + mEatRunOnAnimationRequest = true; - void dispatchPendingImportantForAccessibilityChanges() { - for (int i = mPendingAccessibilityImportanceChange.size() - 1; i >= 0; i--) { - ViewHolder viewHolder = mPendingAccessibilityImportanceChange.get(i); - if (viewHolder.itemView.getParent() != this || viewHolder.shouldIgnore()) { - continue; - } - int state = viewHolder.mPendingAccessibilityState; - if (state != ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET) { - //noinspection WrongConstant - ViewCompat.setImportantForAccessibility(viewHolder.itemView, state); - viewHolder.mPendingAccessibilityState = - ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET; - } - } - mPendingAccessibilityImportanceChange.clear(); - } + consumePendingUpdateOperations(); - int getAdapterPositionInRecyclerView(ViewHolder viewHolder) { - if (viewHolder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID - | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN) - || !viewHolder.isBound()) { - return NO_POSITION; - } - return mAdapterHelper.applyPendingUpdatesToPosition(viewHolder.mPosition); - } + // TODO(72745539): After reviewing the code, it seems to me we may actually want to + // update the reference to the OverScroller after onAnimation. It looks to me like + // it is possible that a new OverScroller could be created (due to a new Interpolator + // being used), when the current OverScroller knows it's done after + // scroller.computeScrollOffset() is called. If that happens, and we don't update the + // reference, it seems to me that we could prematurely stop the newly created scroller + // due to setScrollState(SCROLL_STATE_IDLE) being called below. - @VisibleForTesting - void initFastScroller(StateListDrawable verticalThumbDrawable, - Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable, - Drawable horizontalTrackDrawable) { - if (verticalThumbDrawable == null || verticalTrackDrawable == null - || horizontalThumbDrawable == null || horizontalTrackDrawable == null) { - throw new IllegalArgumentException( - "Trying to set fast scroller without both required drawables." - + exceptionLabel()); - } + // Keep a local reference so that if it is changed during onAnimation method, it won't + // cause unexpected behaviors + final OverScroller scroller = mOverScroller; + if (scroller.computeScrollOffset()) { + final int x = scroller.getCurrX(); + final int y = scroller.getCurrY(); + int unconsumedX = x - mLastFlingX; + int unconsumedY = y - mLastFlingY; + mLastFlingX = x; + mLastFlingY = y; - Resources resources = getContext().getResources(); - new FastScroller(this, verticalThumbDrawable, verticalTrackDrawable, - horizontalThumbDrawable, horizontalTrackDrawable, - resources.getDimensionPixelSize(R.dimen.fastscroll_default_thickness), - resources.getDimensionPixelSize(R.dimen.fastscroll_minimum_range), - resources.getDimensionPixelOffset(R.dimen.fastscroll_margin)); - } + unconsumedX = consumeFlingInHorizontalStretch(unconsumedX); + unconsumedY = consumeFlingInVerticalStretch(unconsumedY); - @Override - public boolean isNestedScrollingEnabled() { - return getScrollingChildHelper().isNestedScrollingEnabled(); - } + int consumedX = 0; + int consumedY = 0; - @Override - public void setNestedScrollingEnabled(boolean enabled) { - getScrollingChildHelper().setNestedScrollingEnabled(enabled); - } + // Nested Pre Scroll + mReusableIntPair[0] = 0; + mReusableIntPair[1] = 0; + if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null, + TYPE_NON_TOUCH)) { + unconsumedX -= mReusableIntPair[0]; + unconsumedY -= mReusableIntPair[1]; + } - @Override - public boolean startNestedScroll(int axes) { - return getScrollingChildHelper().startNestedScroll(axes); - } + // Based on movement, we may want to trigger the hiding of existing over scroll + // glows. + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + considerReleasingGlowsOnScroll(unconsumedX, unconsumedY); + } - @Override - public boolean startNestedScroll(int axes, int type) { - return getScrollingChildHelper().startNestedScroll(axes, type); - } + // Local Scroll + if (mAdapter != null) { + mReusableIntPair[0] = 0; + mReusableIntPair[1] = 0; + scrollStep(unconsumedX, unconsumedY, mReusableIntPair); + consumedX = mReusableIntPair[0]; + consumedY = mReusableIntPair[1]; + unconsumedX -= consumedX; + unconsumedY -= consumedY; - @Override - public void stopNestedScroll() { - getScrollingChildHelper().stopNestedScroll(); - } + // If SmoothScroller exists, this ViewFlinger was started by it, so we must + // report back to SmoothScroller. + SmoothScroller smoothScroller = mLayout.mSmoothScroller; + if (smoothScroller != null && !smoothScroller.isPendingInitialRun() + && smoothScroller.isRunning()) { + final int adapterSize = mState.getItemCount(); + if (adapterSize == 0) { + smoothScroller.stop(); + } else if (smoothScroller.getTargetPosition() >= adapterSize) { + smoothScroller.setTargetPosition(adapterSize - 1); + smoothScroller.onAnimation(consumedX, consumedY); + } else { + smoothScroller.onAnimation(consumedX, consumedY); + } + } + } - @Override - public void stopNestedScroll(int type) { - getScrollingChildHelper().stopNestedScroll(type); - } + if (!mItemDecorations.isEmpty()) { + invalidate(); + } - @Override - public boolean hasNestedScrollingParent() { - return getScrollingChildHelper().hasNestedScrollingParent(); - } + // Nested Post Scroll + mReusableIntPair[0] = 0; + mReusableIntPair[1] = 0; + dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null, + TYPE_NON_TOUCH, mReusableIntPair); + unconsumedX -= mReusableIntPair[0]; + unconsumedY -= mReusableIntPair[1]; - @Override - public boolean hasNestedScrollingParent(int type) { - return getScrollingChildHelper().hasNestedScrollingParent(type); - } + if (consumedX != 0 || consumedY != 0) { + dispatchOnScrolled(consumedX, consumedY); + } - @Override - public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, - int dyUnconsumed, int[] offsetInWindow) { - return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, - dxUnconsumed, dyUnconsumed, offsetInWindow); - } + if (!awakenScrollBars()) { + invalidate(); + } - @Override - public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, - int dyUnconsumed, int[] offsetInWindow, int type) { - return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, - dxUnconsumed, dyUnconsumed, offsetInWindow, type); - } + // We are done scrolling if scroller is finished, or for both the x and y dimension, + // we are done scrolling or we can't scroll further (we know we can't scroll further + // when we have unconsumed scroll distance). It's possible that we don't need + // to also check for scroller.isFinished() at all, but no harm in doing so in case + // of old bugs in Overscroller. + boolean scrollerFinishedX = scroller.getCurrX() == scroller.getFinalX(); + boolean scrollerFinishedY = scroller.getCurrY() == scroller.getFinalY(); + final boolean doneScrolling = scroller.isFinished() + || ((scrollerFinishedX || unconsumedX != 0) + && (scrollerFinishedY || unconsumedY != 0)); - @Override - public final void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, - int dyUnconsumed, int[] offsetInWindow, int type, @NonNull int[] consumed) { - getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, - dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed); - } + // Get the current smoothScroller. It may have changed by this point and we need to + // make sure we don't stop scrolling if it has changed and it's pending an initial + // run. + SmoothScroller smoothScroller = mLayout.mSmoothScroller; + boolean smoothScrollerPending = + smoothScroller != null && smoothScroller.isPendingInitialRun(); - @Override - public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { - return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); - } + if (!smoothScrollerPending && doneScrolling) { + // If we are done scrolling and the layout's SmoothScroller is not pending, + // do the things we do at the end of a scroll and don't postOnAnimation. - @Override - public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, - int type) { - return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, - type); - } + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + final int vel = (int) scroller.getCurrVelocity(); + int velX = unconsumedX < 0 ? -vel : unconsumedX > 0 ? vel : 0; + int velY = unconsumedY < 0 ? -vel : unconsumedY > 0 ? vel : 0; + absorbGlows(velX, velY); + } - @Override - public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { - return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed); - } + if (ALLOW_THREAD_GAP_WORK) { + mPrefetchRegistry.clearPrefetchPositions(); + } + } else { + // Otherwise continue the scroll. - @Override - public boolean dispatchNestedPreFling(float velocityX, float velocityY) { - return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY); - } + postOnAnimation(); + if (mGapWorker != null) { + mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY); + } + } + } - @Override - protected int getChildDrawingOrder(int childCount, int i) { - if (mChildDrawingOrderCallback == null) { - return super.getChildDrawingOrder(childCount, i); - } else { - return mChildDrawingOrderCallback.onGetChildDrawingOrder(childCount, i); - } - } + SmoothScroller smoothScroller = mLayout.mSmoothScroller; + // call this after the onAnimation is complete not to have inconsistent callbacks etc. + if (smoothScroller != null && smoothScroller.isPendingInitialRun()) { + smoothScroller.onAnimation(0, 0); + } - private NestedScrollingChildHelper getScrollingChildHelper() { - if (mScrollingChildHelper == null) { - mScrollingChildHelper = new NestedScrollingChildHelper(this); + mEatRunOnAnimationRequest = false; + if (mReSchedulePostAnimationCallback) { + internalPostOnAnimation(); + } else { + setScrollState(SCROLL_STATE_IDLE); + stopNestedScroll(TYPE_NON_TOUCH); + } } - return mScrollingChildHelper; - } - // NestedScrollingChild + void postOnAnimation() { + if (mEatRunOnAnimationRequest) { + mReSchedulePostAnimationCallback = true; + } else { + internalPostOnAnimation(); + } + } - /** - * @hide - */ - @RestrictTo(LIBRARY_GROUP_PREFIX) - @IntDef({HORIZONTAL, VERTICAL}) - @Retention(RetentionPolicy.SOURCE) - public @interface Orientation { - } + private void internalPostOnAnimation() { + removeCallbacks(this); + ViewCompat.postOnAnimation(RecyclerView.this, this); + } - /** - * An OnItemTouchListener allows the application to intercept touch events in progress at the - * view hierarchy level of the RecyclerView before those touch events are considered for - * RecyclerView's own scrolling behavior. - * - *

    This can be useful for applications that wish to implement various forms of gestural - * manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept - * a touch interaction already in progress even if the RecyclerView is already handling that - * gesture stream itself for the purposes of scrolling.

    - * - * @see SimpleOnItemTouchListener - */ - public interface OnItemTouchListener { - /** - * Silently observe and/or take over touch events sent to the RecyclerView - * before they are handled by either the RecyclerView itself or its child views. - * - *

    The onInterceptTouchEvent methods of each attached OnItemTouchListener will be run - * in the order in which each listener was added, before any other touch processing - * by the RecyclerView itself or child views occurs.

    - * - * @param e MotionEvent describing the touch event. All coordinates are in - * the RecyclerView's coordinate system. - * @return true if this OnItemTouchListener wishes to begin intercepting touch events, false - * to continue with the current behavior and continue observing future events in - * the gesture. - */ - boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e); + public void fling(int velocityX, int velocityY) { + setScrollState(SCROLL_STATE_SETTLING); + mLastFlingX = mLastFlingY = 0; + // Because you can't define a custom interpolator for flinging, we should make sure we + // reset ourselves back to the teh default interpolator in case a different call + // changed our interpolator. + if (mInterpolator != sQuinticInterpolator) { + mInterpolator = sQuinticInterpolator; + mOverScroller = new OverScroller(getContext(), sQuinticInterpolator); + } + mOverScroller.fling(0, 0, velocityX, velocityY, + Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); + postOnAnimation(); + } /** - * Process a touch event as part of a gesture that was claimed by returning true from - * a previous call to {@link #onInterceptTouchEvent}. + * Smooth scrolls the RecyclerView by a given distance. * - * @param e MotionEvent describing the touch event. All coordinates are in - * the RecyclerView's coordinate system. + * @param dx x distance in pixels. + * @param dy y distance in pixels. + * @param duration Duration of the animation in milliseconds. Set to + * {@link #UNDEFINED_DURATION} to have the duration automatically + * calculated + * based on an internally defined standard velocity. + * @param interpolator {@link Interpolator} to be used for scrolling. If it is {@code null}, + * RecyclerView will use an internal default interpolator. */ - void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e); + public void smoothScrollBy(int dx, int dy, int duration, + @Nullable Interpolator interpolator) { - /** - * Called when a child of RecyclerView does not want RecyclerView and its ancestors to - * intercept touch events with - * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}. - * - * @param disallowIntercept True if the child does not want the parent to - * intercept touch events. - * @see ViewParent#requestDisallowInterceptTouchEvent(boolean) - */ - void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept); - } + // Handle cases where parameter values aren't defined. + if (duration == UNDEFINED_DURATION) { + duration = computeScrollDuration(dx, dy); + } + if (interpolator == null) { + interpolator = sQuinticInterpolator; + } - /** - * A RecyclerListener can be set on a RecyclerView to receive messages whenever - * a view is recycled. - * - * @see RecyclerView#setRecyclerListener(RecyclerListener) - */ - public interface RecyclerListener { + // If the Interpolator has changed, create a new OverScroller with the new + // interpolator. + if (mInterpolator != interpolator) { + mInterpolator = interpolator; + mOverScroller = new OverScroller(getContext(), interpolator); + } - /** - * This method is called whenever the view in the ViewHolder is recycled. - *

    - * RecyclerView calls this method right before clearing ViewHolder's internal data and - * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information - * before being recycled, you can call {@link ViewHolder#getBindingAdapterPosition()} to get - * its adapter position. - * - * @param holder The ViewHolder containing the view that was recycled - */ - void onViewRecycled(@NonNull ViewHolder holder); - } + // Reset the last fling information. + mLastFlingX = mLastFlingY = 0; - /** - * A Listener interface that can be attached to a RecylcerView to get notified - * whenever a ViewHolder is attached to or detached from RecyclerView. - */ - public interface OnChildAttachStateChangeListener { + // Set to settling state and start scrolling. + setScrollState(SCROLL_STATE_SETTLING); + mOverScroller.startScroll(0, 0, dx, dy, duration); - /** - * Called when a view is attached to the RecyclerView. - * - * @param view The View which is attached to the RecyclerView - */ - void onChildViewAttachedToWindow(@NonNull View view); + if (Build.VERSION.SDK_INT < 23) { + // b/64931938 before API 23, startScroll() does not reset getCurX()/getCurY() + // to start values, which causes fillRemainingScrollValues() put in obsolete values + // for LayoutManager.onLayoutChildren(). + mOverScroller.computeScrollOffset(); + } + + postOnAnimation(); + } /** - * Called when a view is detached from RecyclerView. - * - * @param view The View which is being detached from the RecyclerView + * Computes of an animated scroll in milliseconds. + * @param dx x distance in pixels. + * @param dy y distance in pixels. + * @return The duration of the animated scroll in milliseconds. */ - void onChildViewDetachedFromWindow(@NonNull View view); + private int computeScrollDuration(int dx, int dy) { + final int absDx = Math.abs(dx); + final int absDy = Math.abs(dy); + final boolean horizontal = absDx > absDy; + final int containerSize = horizontal ? getWidth() : getHeight(); + + float absDelta = (float) (horizontal ? absDx : absDy); + final int duration = (int) (((absDelta / containerSize) + 1) * 300); + + return Math.min(duration, MAX_SCROLL_DURATION); + } + + public void stop() { + removeCallbacks(this); + mOverScroller.abortAnimation(); + } + } - /** - * A callback interface that can be used to alter the drawing order of RecyclerView children. - *

    - * It works using the {@link ViewGroup#getChildDrawingOrder(int, int)} method, so any case - * that applies to that method also applies to this callback. For example, changing the drawing - * order of two views will not have any effect if their elevation values are different since - * elevation overrides the result of this callback. - */ - public interface ChildDrawingOrderCallback { - /** - * Returns the index of the child to draw for this iteration. Override this - * if you want to change the drawing order of children. By default, it - * returns i. - * - * @param i The current iteration. - * @return The index of the child to draw this iteration. - * @see RecyclerView#setChildDrawingOrderCallback(RecyclerView.ChildDrawingOrderCallback) - */ - int onGetChildDrawingOrder(int childCount, int i); + void repositionShadowingViews() { + // Fix up shadow views used by change animations + int count = mChildHelper.getChildCount(); + for (int i = 0; i < count; i++) { + View view = mChildHelper.getChildAt(i); + ViewHolder holder = getChildViewHolder(view); + if (holder != null && holder.mShadowingHolder != null) { + View shadowingView = holder.mShadowingHolder.itemView; + int left = view.getLeft(); + int top = view.getTop(); + if (left != shadowingView.getLeft() || top != shadowingView.getTop()) { + shadowingView.layout(left, top, + left + shadowingView.getWidth(), + top + shadowingView.getHeight()); + } + } + } + } + + private class RecyclerViewDataObserver extends AdapterDataObserver { + RecyclerViewDataObserver() { + } + + @Override + public void onChanged() { + assertNotInLayoutOrScroll(null); + mState.mStructureChanged = true; + + processDataSetCompletelyChanged(true); + if (!mAdapterHelper.hasPendingUpdates()) { + requestLayout(); + } + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) { + triggerUpdateProcessor(); + } + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) { + triggerUpdateProcessor(); + } + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) { + triggerUpdateProcessor(); + } + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) { + triggerUpdateProcessor(); + } + } + + void triggerUpdateProcessor() { + if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) { + ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable); + } else { + mAdapterUpdateDuringMeasure = true; + requestLayout(); + } + } + + @Override + public void onStateRestorationPolicyChanged() { + if (mPendingSavedState == null) { + return; + } + // If there is a pending saved state and the new mode requires us to restore it, + // we'll request a layout which will call the adapter to see if it can restore state + // and trigger state restoration + Adapter adapter = mAdapter; + if (adapter != null && adapter.canRestoreState()) { + requestLayout(); + } + } } /** @@ -6033,18 +6086,26 @@ public interface ChildDrawingOrderCallback { */ public static class EdgeEffectFactory { + @Retention(RetentionPolicy.SOURCE) + @IntDef({DIRECTION_LEFT, DIRECTION_TOP, DIRECTION_RIGHT, DIRECTION_BOTTOM}) + public @interface EdgeDirection { + } + /** * Direction constant for the left edge */ public static final int DIRECTION_LEFT = 0; + /** * Direction constant for the top edge */ public static final int DIRECTION_TOP = 1; + /** * Direction constant for the right edge */ public static final int DIRECTION_RIGHT = 2; + /** * Direction constant for the bottom edge */ @@ -6054,15 +6115,10 @@ public static class EdgeEffectFactory { * Create a new EdgeEffect for the provided direction. */ protected @NonNull - EdgeEffect createEdgeEffect(@NonNull RecyclerView view, - @EdgeDirection int direction) { + EdgeEffect createEdgeEffect(@NonNull RecyclerView view, + @EdgeDirection int direction) { return new EdgeEffect(view.getContext()); } - - @Retention(RetentionPolicy.SOURCE) - @IntDef({DIRECTION_LEFT, DIRECTION_TOP, DIRECTION_RIGHT, DIRECTION_BOTTOM}) - public @interface EdgeDirection { - } } /** @@ -6086,25 +6142,41 @@ protected EdgeEffect createEdgeEffect(@NonNull RecyclerView view, int direction) */ public static class RecycledViewPool { private static final int DEFAULT_MAX_SCRAP = 5; - final SparseArray mScrap = new SparseArray<>(); + /** - * The set of adapters for PoolingContainer release purposes + * Tracks both pooled holders, as well as create/bind timing metadata for the given type. * - * @see #mAttachCountForClearing + * Note that this tracks running averages of create/bind time across all RecyclerViews + * (and, indirectly, Adapters) that use this pool. + * + * 1) This enables us to track average create and bind times across multiple adapters. Even + * though create (and especially bind) may behave differently for different Adapter + * subclasses, sharing the pool is a strong signal that they'll perform similarly, per type. + * + * 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return + * false for all other views of its type for the same deadline. This prevents items + * constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch. */ - final Set> mAttachedAdaptersForPoolingContainer = - Collections.newSetFromMap(new IdentityHashMap<>()); + static class ScrapData { + final ArrayList mScrapHeap = new ArrayList<>(); + int mMaxScrap = DEFAULT_MAX_SCRAP; + long mCreateRunningAverageNs = 0; + long mBindRunningAverageNs = 0; + } + + SparseArray mScrap = new SparseArray<>(); + /** * Attach counts for clearing (that is, emptying the pool when there are no adapters * attached) and for PoolingContainer release are tracked separately to maintain the * historical behavior of this functionality. - *

    + * * The count for clearing is inaccurate in certain scenarios: for instance, if a * RecyclerView is removed from the view hierarchy and thrown away to be GCed, the * attach count will never be correspondingly decreased. However, it has been this way * for years without any complaints, so we are not going to potentially increase the * number of scenarios where the pool would be cleared. - *

    + * * The attached adapters for PoolingContainer purposes strives to be more accurate, as * it will be decremented whenever a RecyclerView is detached from the window. This * could potentially be inaccurate in the unlikely event that someone is manually driving @@ -6112,7 +6184,15 @@ public static class RecycledViewPool { * implementation of {@link RecyclerView#onDetachedFromWindow()} suggests this is not the * only unexpected behavior that doing so might provoke, so this should be acceptable. */ - int mAttachCountForClearing; + int mAttachCountForClearing = 0; + + /** + * The set of adapters for PoolingContainer release purposes + * + * @see #mAttachCountForClearing + */ + Set> mAttachedAdaptersForPoolingContainer = + Collections.newSetFromMap(new IdentityHashMap<>()); /** * Discard all ViewHolders. @@ -6120,7 +6200,7 @@ public static class RecycledViewPool { public void clear() { for (int i = 0; i < mScrap.size(); i++) { ScrapData data = mScrap.valueAt(i); - for (ViewHolder scrap : data.mScrapHeap) { + for (ViewHolder scrap: data.mScrapHeap) { PoolingContainer.callPoolingContainerOnRelease(scrap.itemView); } data.mScrapHeap.clear(); @@ -6136,7 +6216,7 @@ public void clear() { public void setMaxRecycledViews(int viewType, int max) { ScrapData scrapData = getScrapDataForType(viewType); scrapData.mMaxScrap = max; - ArrayList scrapHeap = scrapData.mScrapHeap; + final ArrayList scrapHeap = scrapData.mScrapHeap; while (scrapHeap.size() > max) { scrapHeap.remove(scrapHeap.size() - 1); } @@ -6159,9 +6239,9 @@ public int getRecycledViewCount(int viewType) { */ @Nullable public ViewHolder getRecycledView(int viewType) { - ScrapData scrapData = mScrap.get(viewType); + final ScrapData scrapData = mScrap.get(viewType); if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) { - ArrayList scrapHeap = scrapData.mScrapHeap; + final ArrayList scrapHeap = scrapData.mScrapHeap; for (int i = scrapHeap.size() - 1; i >= 0; i--) { if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) { return scrapHeap.remove(i); @@ -6195,8 +6275,8 @@ int size() { * @param scrap ViewHolder to be added to the pool. */ public void putRecycledView(ViewHolder scrap) { - int viewType = scrap.getItemViewType(); - ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap; + final int viewType = scrap.getItemViewType(); + final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap; if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { PoolingContainer.callPoolingContainerOnRelease(scrap.itemView); return; @@ -6259,12 +6339,12 @@ void attachForPoolingContainer(@NonNull Adapter adapter) { /** * Removes this adapter from the set of adapters being tracked for PoolingContainer * release purposes. This method may validly be called multiple times for a given adapter. - * + Additional calls to this method for an already-detached adapter are a no-op. + + Additional calls to this method for an already-detached adapter are a no-op. * - * @param adapter the adapter to be removed from the set + * @param adapter the adapter to be removed from the set * @param isBeingReplaced {@code true} if this detach is immediately preceding a call to - * {@link #attachForPoolingContainer(Adapter)} and - * {@link PoolingContainerListener#onRelease()} should not be triggered, or false otherwise + * {@link #attachForPoolingContainer(Adapter)} and + * {@link PoolingContainerListener#onRelease()} should not be triggered, or false otherwise */ void detachForPoolingContainer(@NonNull Adapter adapter, boolean isBeingReplaced) { mAttachedAdaptersForPoolingContainer.remove(adapter); @@ -6292,7 +6372,7 @@ void detachForPoolingContainer(@NonNull Adapter adapter, boolean isBeingRepla * ViewHolder and view types. */ void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter, - boolean compatibleWithPrevious) { + boolean compatibleWithPrevious) { if (oldAdapter != null) { detach(); } @@ -6312,7919 +6392,7987 @@ private ScrapData getScrapDataForType(int viewType) { } return scrapData; } + } - /** - * Tracks both pooled holders, as well as create/bind timing metadata for the given type. - *

    - * Note that this tracks running averages of create/bind time across all RecyclerViews - * (and, indirectly, Adapters) that use this pool. - *

    - * 1) This enables us to track average create and bind times across multiple adapters. Even - * though create (and especially bind) may behave differently for different Adapter - * subclasses, sharing the pool is a strong signal that they'll perform similarly, per type. - *

    - * 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return - * false for all other views of its type for the same deadline. This prevents items - * constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch. - */ - static class ScrapData { - final ArrayList mScrapHeap = new ArrayList<>(); - int mMaxScrap = DEFAULT_MAX_SCRAP; - long mCreateRunningAverageNs; - long mBindRunningAverageNs; + /** + * Utility method for finding an internal RecyclerView, if present + */ + @Nullable + static RecyclerView findNestedRecyclerView(@NonNull View view) { + if (!(view instanceof ViewGroup)) { + return null; } + if (view instanceof RecyclerView) { + return (RecyclerView) view; + } + final ViewGroup parent = (ViewGroup) view; + final int count = parent.getChildCount(); + for (int i = 0; i < count; i++) { + final View child = parent.getChildAt(i); + final RecyclerView descendant = findNestedRecyclerView(child); + if (descendant != null) { + return descendant; + } + } + return null; } /** - * ViewCacheExtension is a helper class to provide an additional layer of view caching that can - * be controlled by the developer. - *

    - * When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and - * first level cache to find a matching View. If it cannot find a suitable View, Recycler will - * call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking - * {@link RecycledViewPool}. - *

    - * Note that, Recycler never sends Views to this method to be cached. It is developers - * responsibility to decide whether they want to keep their Views in this custom cache or let - * the default recycling policy handle it. + * Utility method for clearing holder's internal RecyclerView, if present */ - public abstract static class ViewCacheExtension { + static void clearNestedRecyclerViewIfNotNested(@NonNull ViewHolder holder) { + if (holder.mNestedRecyclerView != null) { + View item = holder.mNestedRecyclerView.get(); + while (item != null) { + if (item == holder.itemView) { + return; // match found, don't need to clear + } - /** - * Returns a View that can be binded to the given Adapter position. - *

    - * This method should not create a new View. Instead, it is expected to return - * an already created View that can be re-used for the given type and position. - * If the View is marked as ignored, it should first call - * {@link LayoutManager#stopIgnoringView(View)} before returning the View. - *

    - * RecyclerView will re-bind the returned View to the position if necessary. - * - * @param recycler The Recycler that can be used to bind the View - * @param position The adapter position - * @param type The type of the View, defined by adapter - * @return A View that is bound to the given position or NULL if there is no View to re-use - * @see LayoutManager#ignoreView(View) - */ - @Nullable - public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, - int type); + ViewParent parent = item.getParent(); + if (parent instanceof View) { + item = (View) parent; + } else { + item = null; + } + } + holder.mNestedRecyclerView = null; // not nested + } } /** - * Base class for an Adapter + * Time base for deadline-aware work scheduling. Overridable for testing. * - *

    Adapters provide a binding from an app-specific data set to views that are displayed - * within a {@link RecyclerView}.

    + * Will return 0 to avoid cost of System.nanoTime where deadline-aware work scheduling + * isn't relevant. + */ + long getNanoTime() { + if (ALLOW_THREAD_GAP_WORK) { + return System.nanoTime(); + } else { + return 0; + } + } + + /** + * A Recycler is responsible for managing scrapped or detached item views for reuse. * - * @param A class that extends ViewHolder that will be used by the adapter. + *

    A "scrapped" view is a view that is still attached to its parent RecyclerView but + * that has been marked for removal or reuse.

    + * + *

    Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for + * an adapter's data set representing the data at a given position or item ID. + * If the view to be reused is considered "dirty" the adapter will be asked to rebind it. + * If not, the view can be quickly reused by the LayoutManager with no further work. + * Clean views that have not {@link android.view.View#isLayoutRequested() requested layout} + * may be repositioned by a LayoutManager without remeasurement.

    */ - public abstract static class Adapter { - private final AdapterDataObservable mObservable = new AdapterDataObservable(); - private boolean mHasStableIds; - private StateRestorationPolicy mStateRestorationPolicy = StateRestorationPolicy.ALLOW; + public final class Recycler { + final ArrayList mAttachedScrap = new ArrayList<>(); + ArrayList mChangedScrap = null; + + final ArrayList mCachedViews = new ArrayList<>(); + + private final List + mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap); + + private int mRequestedCacheMax = DEFAULT_CACHE_SIZE; + int mViewCacheMax = DEFAULT_CACHE_SIZE; + + RecycledViewPool mRecyclerPool; + + private ViewCacheExtension mViewCacheExtension; + + static final int DEFAULT_CACHE_SIZE = 2; /** - * Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent - * an item. - *

    - * This new ViewHolder should be constructed with a new View that can represent the items - * of the given type. You can either create a new View manually or inflate it from an XML - * layout file. - *

    - * The new ViewHolder will be used to display items of the adapter using - * {@link #onBindViewHolder(ViewHolder, int, List)}. Since it will be re-used to display - * different items in the data set, it is a good idea to cache references to sub views of - * the View to avoid unnecessary {@link View#findViewById(int)} calls. + * Clear scrap views out of this recycler. Detached views contained within a + * recycled view pool will remain. + */ + public void clear() { + mAttachedScrap.clear(); + recycleAndClearCachedViews(); + } + + /** + * Set the maximum number of detached, valid views we should retain for later use. * - * @param parent The ViewGroup into which the new View will be added after it is bound to - * an adapter position. - * @param viewType The view type of the new View. - * @return A new ViewHolder that holds a View of the given view type. - * @see #getItemViewType(int) - * @see #onBindViewHolder(ViewHolder, int) + * @param viewCount Number of views to keep before sending views to the shared pool */ - @NonNull - public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType); + public void setViewCacheSize(int viewCount) { + mRequestedCacheMax = viewCount; + updateViewCacheSize(); + } + + void updateViewCacheSize() { + int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0; + mViewCacheMax = mRequestedCacheMax + extraCache; + + // first, try the views that can be recycled + for (int i = mCachedViews.size() - 1; + i >= 0 && mCachedViews.size() > mViewCacheMax; i--) { + recycleCachedViewAt(i); + } + } /** - * Called by RecyclerView to display the data at the specified position. This method should - * update the contents of the {@link ViewHolder#itemView} to reflect the item at the given - * position. - *

    - * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method - * again if the position of the item changes in the data set unless the item itself is - * invalidated or the new position cannot be determined. For this reason, you should only - * use the position parameter while acquiring the related data item inside - * this method and should not keep a copy of it. If you need the position of an item later - * on (e.g. in a click listener), use {@link ViewHolder#getBindingAdapterPosition()} which - * will have the updated adapter position. - *

    - * Override {@link #onBindViewHolder(ViewHolder, int, List)} instead if Adapter can - * handle efficient partial bind. + * Returns an unmodifiable list of ViewHolders that are currently in the scrap list. * - * @param holder The ViewHolder which should be updated to represent the contents of the - * item at the given position in the data set. - * @param position The position of the item within the adapter's data set. + * @return List of ViewHolders in the scrap list. */ - public abstract void onBindViewHolder(@NonNull VH holder, int position); + @NonNull + public List getScrapList() { + return mUnmodifiableAttachedScrap; + } /** - * Called by RecyclerView to display the data at the specified position. This method - * should update the contents of the {@link ViewHolder#itemView} to reflect the item at - * the given position. - *

    - * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method - * again if the position of the item changes in the data set unless the item itself is - * invalidated or the new position cannot be determined. For this reason, you should only - * use the position parameter while acquiring the related data item inside - * this method and should not keep a copy of it. If you need the position of an item later - * on (e.g. in a click listener), use {@link ViewHolder#getBindingAdapterPosition()} which - * will have the updated adapter position. - *

    - * Partial bind vs full bind: + * Helper method for getViewForPosition. *

    - * The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or - * {@link #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty, - * the ViewHolder is currently bound to old data and Adapter may run an efficient partial - * update using the payload info. If the payload is empty, Adapter must run a full bind. - * Adapter should not assume that the payload passed in notify methods will be received by - * onBindViewHolder(). For example when the view is not attached to the screen, the - * payload in notifyItemChange() will be simply dropped. + * Checks whether a given view holder can be used for the provided position. * - * @param holder The ViewHolder which should be updated to represent the contents of the - * item at the given position in the data set. - * @param position The position of the item within the adapter's data set. - * @param payloads A non-null list of merged payloads. Can be empty list if requires full - * update. + * @param holder ViewHolder + * @return true if ViewHolder matches the provided position, false otherwise */ - public void onBindViewHolder(@NonNull VH holder, int position, - @NonNull List payloads) { - onBindViewHolder(holder, position); + boolean validateViewHolderForOffsetPosition(ViewHolder holder) { + // if it is a removed holder, nothing to verify since we cannot ask adapter anymore + // if it is not removed, verify the type and id. + if (holder.isRemoved()) { + if (DEBUG && !mState.isPreLayout()) { + throw new IllegalStateException("should not receive a removed view unless it" + + " is pre layout" + exceptionLabel()); + } + return mState.isPreLayout(); + } + if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) { + throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder " + + "adapter position" + holder + exceptionLabel()); + } + if (!mState.isPreLayout()) { + // don't check type if it is pre-layout. + final int type = mAdapter.getItemViewType(holder.mPosition); + if (type != holder.getItemViewType()) { + return false; + } + } + if (mAdapter.hasStableIds()) { + return holder.getItemId() == mAdapter.getItemId(holder.mPosition); + } + return true; } /** - * Returns the position of the given {@link ViewHolder} in the given {@link Adapter}. - *

    - * If the given {@link Adapter} is not part of this {@link Adapter}, - * {@link RecyclerView#NO_POSITION} is returned. + * Attempts to bind view, and account for relevant timing information. If + * deadlineNs != FOREVER_NS, this method may fail to bind, and return false. * - * @param adapter The adapter which is a sub adapter of this adapter or itself. - * @param viewHolder The ViewHolder whose local position in the given adapter will be - * returned. - * @param localPosition The position of the given {@link ViewHolder} in this - * {@link Adapter}. - * @return The local position of the given {@link ViewHolder} in this {@link Adapter} - * or {@link RecyclerView#NO_POSITION} if the {@link ViewHolder} is not bound to an item - * or the given {@link Adapter} is not part of this Adapter (if this Adapter merges other - * adapters). + * @param holder Holder to be bound. + * @param offsetPosition Position of item to be bound. + * @param position Pre-layout position of item to be bound. + * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should + * complete. If FOREVER_NS is passed, this method will not fail to + * bind the holder. */ - public int findRelativeAdapterPositionIn( - @NonNull Adapter adapter, - @NonNull ViewHolder viewHolder, - int localPosition - ) { - if (adapter == this) { - return localPosition; + @SuppressWarnings("unchecked") + private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition, + int position, long deadlineNs) { + holder.mBindingAdapter = null; + holder.mOwnerRecyclerView = RecyclerView.this; + final int viewType = holder.getItemViewType(); + long startBindNs = getNanoTime(); + if (deadlineNs != FOREVER_NS + && !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) { + // abort - we have a deadline we can't meet + return false; } - return NO_POSITION; + mAdapter.bindViewHolder(holder, offsetPosition); + long endBindNs = getNanoTime(); + mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs); + attachAccessibilityDelegateOnBind(holder); + if (mState.isPreLayout()) { + holder.mPreLayoutPosition = position; + } + return true; } /** - * This method calls {@link #onCreateViewHolder(ViewGroup, int)} to create a new - * {@link ViewHolder} and initializes some private fields to be used by RecyclerView. + * Binds the given View to the position. The View can be a View previously retrieved via + * {@link #getViewForPosition(int)} or created by + * {@link Adapter#onCreateViewHolder(ViewGroup, int)}. + *

    + * Generally, a LayoutManager should acquire its views via {@link #getViewForPosition(int)} + * and let the RecyclerView handle caching. This is a helper method for LayoutManager who + * wants to handle its own recycling logic. + *

    + * Note that, {@link #getViewForPosition(int)} already binds the View to the position so + * you don't need to call this method unless you want to bind this View to another position. * - * @see #onCreateViewHolder(ViewGroup, int) + * @param view The view to update. + * @param position The position of the item to bind to this View. */ - @NonNull - public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) { - try { - Trace.beginSection(TRACE_CREATE_VIEW_TAG); - VH holder = onCreateViewHolder(parent, viewType); - if (holder.itemView.getParent() != null) { - throw new IllegalStateException("ViewHolder views must not be attached when" - + " created. Ensure that you are not passing 'true' to the attachToRoot" - + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)"); - } - holder.mItemViewType = viewType; - return holder; - } finally { - Trace.endSection(); + public void bindViewToPosition(@NonNull View view, int position) { + ViewHolder holder = getChildViewHolderInt(view); + if (holder == null) { + throw new IllegalArgumentException("The view does not have a ViewHolder. You cannot" + + " pass arbitrary views to this method, they should be created by the " + + "Adapter" + exceptionLabel()); + } + final int offsetPosition = mAdapterHelper.findPositionOffset(position); + if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { + throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " + + "position " + position + "(offset:" + offsetPosition + ")." + + "state:" + mState.getItemCount() + exceptionLabel()); + } + tryBindViewHolderByDeadline(holder, offsetPosition, position, FOREVER_NS); + + final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); + final LayoutParams rvLayoutParams; + if (lp == null) { + rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); + holder.itemView.setLayoutParams(rvLayoutParams); + } else if (!checkLayoutParams(lp)) { + rvLayoutParams = (LayoutParams) generateLayoutParams(lp); + holder.itemView.setLayoutParams(rvLayoutParams); + } else { + rvLayoutParams = (LayoutParams) lp; } + + rvLayoutParams.mInsetsDirty = true; + rvLayoutParams.mViewHolder = holder; + rvLayoutParams.mPendingInvalidate = holder.itemView.getParent() == null; } /** - * This method internally calls {@link #onBindViewHolder(ViewHolder, int)} to update the - * {@link ViewHolder} contents with the item at the given position and also sets up some - * private fields to be used by RecyclerView. + * RecyclerView provides artificial position range (item count) in pre-layout state and + * automatically maps these positions to {@link Adapter} positions when + * {@link #getViewForPosition(int)} or {@link #bindViewToPosition(View, int)} is called. *

    - * Adapters that merge other adapters should use - * {@link #bindViewHolder(ViewHolder, int)} when calling nested adapters so that - * RecyclerView can track which adapter bound the {@link ViewHolder} to return the correct - * position from {@link ViewHolder#getBindingAdapterPosition()} method. - * They should also override - * the {@link #findRelativeAdapterPositionIn(Adapter, ViewHolder, int)} method. + * Usually, LayoutManager does not need to worry about this. However, in some cases, your + * LayoutManager may need to call some custom component with item positions in which + * case you need the actual adapter position instead of the pre layout position. You + * can use this method to convert a pre-layout position to adapter (post layout) position. + *

    + * Note that if the provided position belongs to a deleted ViewHolder, this method will + * return -1. + *

    + * Calling this method in post-layout state returns the same value back. * - * @param holder The view holder whose contents should be updated - * @param position The position of the holder with respect to this adapter - * @see #onBindViewHolder(ViewHolder, int) + * @param position The pre-layout position to convert. Must be greater or equal to 0 and + * less than {@link State#getItemCount()}. */ - public final void bindViewHolder(@NonNull VH holder, int position) { - boolean rootBind = holder.mBindingAdapter == null; - if (rootBind) { - holder.mPosition = position; - if (hasStableIds()) { - holder.mItemId = getItemId(position); - } - holder.setFlags(ViewHolder.FLAG_BOUND, - ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID - | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN); - Trace.beginSection(TRACE_BIND_VIEW_TAG); + public int convertPreLayoutPositionToPostLayout(int position) { + if (position < 0 || position >= mState.getItemCount()) { + throw new IndexOutOfBoundsException("invalid position " + position + ". State " + + "item count is " + mState.getItemCount() + exceptionLabel()); } - holder.mBindingAdapter = this; - onBindViewHolder(holder, position, holder.getUnmodifiedPayloads()); - if (rootBind) { - holder.clearPayload(); - ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams(); - if (layoutParams instanceof RecyclerView.LayoutParams) { - ((LayoutParams) layoutParams).mInsetsDirty = true; - } - Trace.endSection(); + if (!mState.isPreLayout()) { + return position; } + return mAdapterHelper.findPositionOffset(position); } /** - * Return the view type of the item at position for the purposes - * of view recycling. - * - *

    The default implementation of this method returns 0, making the assumption of - * a single view type for the adapter. Unlike ListView adapters, types need not - * be contiguous. Consider using id resources to uniquely identify item view types. + * Obtain a view initialized for the given position. * - * @param position position to query - * @return integer value identifying the type of the view needed to represent the item at - * position. Type codes need not be contiguous. - */ - public int getItemViewType(int position) { - return 0; - } - - /** - * Indicates whether each item in the data set can be represented with a unique identifier - * of type {@link java.lang.Long}. + * This method should be used by {@link LayoutManager} implementations to obtain + * views to represent data from an {@link Adapter}. + *

    + * The Recycler may reuse a scrap or detached view from a shared pool if one is + * available for the correct view type. If the adapter has not indicated that the + * data at the given position has changed, the Recycler will attempt to hand back + * a scrap view that was previously initialized for that data without rebinding. * - * @param hasStableIds Whether items in data set have unique identifiers or not. - * @see #hasStableIds() - * @see #getItemId(int) + * @param position Position to obtain a view for + * @return A view representing the data at position from adapter */ - public void setHasStableIds(boolean hasStableIds) { - if (hasObservers()) { - throw new IllegalStateException("Cannot change whether this adapter has " - + "stable IDs while the adapter has registered observers."); - } - mHasStableIds = hasStableIds; + @NonNull + public View getViewForPosition(int position) { + return getViewForPosition(position, false); } - /** - * Return the stable ID for the item at position. If {@link #hasStableIds()} - * would return false this method should return {@link #NO_ID}. The default implementation - * of this method returns {@link #NO_ID}. - * - * @param position Adapter position to query - * @return the stable ID of the item at position - */ - public long getItemId(int position) { - return NO_ID; + View getViewForPosition(int position, boolean dryRun) { + return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; } /** - * Returns the total number of items in the data set held by the adapter. + * Attempts to get the ViewHolder for the given position, either from the Recycler scrap, + * cache, the RecycledViewPool, or creating it directly. + *

    + * If a deadlineNs other than {@link #FOREVER_NS} is passed, this method early return + * rather than constructing or binding a ViewHolder if it doesn't think it has time. + * If a ViewHolder must be constructed and not enough time remains, null is returned. If a + * ViewHolder is aquired and must be bound but not enough time remains, an unbound holder is + * returned. Use {@link ViewHolder#isBound()} on the returned object to check for this. * - * @return The total number of items in this adapter. + * @param position Position of ViewHolder to be returned. + * @param dryRun True if the ViewHolder should not be removed from scrap/cache/ + * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should + * complete. If FOREVER_NS is passed, this method will not fail to + * create/bind the holder if needed. + * @return ViewHolder for requested position */ - public abstract int getItemCount(); - - /** - * Returns true if this adapter publishes a unique long value that can - * act as a key for the item at a given position in the data set. If that item is relocated - * in the data set, the ID returned for that item should be the same. - * - * @return true if this adapter's items have stable IDs - */ - public final boolean hasStableIds() { - return mHasStableIds; - } - - /** - * Called when a view created by this adapter has been recycled. - * - *

    A view is recycled when a {@link LayoutManager} decides that it no longer - * needs to be attached to its parent {@link RecyclerView}. This can be because it has - * fallen out of visibility or a set of cached views represented by views still - * attached to the parent RecyclerView. If an item view has large or expensive data - * bound to it such as large bitmaps, this may be a good place to release those - * resources.

    - *

    - * RecyclerView calls this method right before clearing ViewHolder's internal data and - * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information - * before being recycled, you can call {@link ViewHolder#getBindingAdapterPosition()} to get - * its adapter position. - * - * @param holder The ViewHolder for the view being recycled - */ - public void onViewRecycled(@NonNull VH holder) { - } - - /** - * Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled - * due to its transient state. Upon receiving this callback, Adapter can clear the - * animation(s) that effect the View's transient state and return true so that - * the View can be recycled. Keep in mind that the View in question is already removed from - * the RecyclerView. - *

    - * In some cases, it is acceptable to recycle a View although it has transient state. Most - * of the time, this is a case where the transient state will be cleared in - * {@link #onBindViewHolder(ViewHolder, int)} call when View is rebound to a new position. - * For this reason, RecyclerView leaves the decision to the Adapter and uses the return - * value of this method to decide whether the View should be recycled or not. - *

    - * Note that when all animations are created by {@link RecyclerView.ItemAnimator}, you - * should never receive this callback because RecyclerView keeps those Views as children - * until their animations are complete. This callback is useful when children of the item - * views create animations which may not be easy to implement using an {@link ItemAnimator}. - *

    - * You should never fix this issue by calling - * holder.itemView.setHasTransientState(false); unless you've previously called - * holder.itemView.setHasTransientState(true);. Each - * View.setHasTransientState(true) call must be matched by a - * View.setHasTransientState(false) call, otherwise, the state of the View - * may become inconsistent. You should always prefer to end or cancel animations that are - * triggering the transient state instead of handling it manually. - * - * @param holder The ViewHolder containing the View that could not be recycled due to its - * transient state. - * @return True if the View should be recycled, false otherwise. Note that if this method - * returns true, RecyclerView will ignore the transient state of - * the View and recycle it regardless. If this method returns false, - * RecyclerView will check the View's transient state again before giving a final decision. - * Default implementation returns false. - */ - public boolean onFailedToRecycleView(@NonNull VH holder) { - return false; - } - - /** - * Called when a view created by this adapter has been attached to a window. - * - *

    This can be used as a reasonable signal that the view is about to be seen - * by the user. If the adapter previously freed any resources in - * {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder) onViewDetachedFromWindow} - * those resources should be restored here.

    - * - * @param holder Holder of the view being attached - */ - public void onViewAttachedToWindow(@NonNull VH holder) { - } + @Nullable + ViewHolder tryGetViewHolderForPositionByDeadline(int position, + boolean dryRun, long deadlineNs) { + if (position < 0 || position >= mState.getItemCount()) { + throw new IndexOutOfBoundsException("Invalid item position " + position + + "(" + position + "). Item count:" + mState.getItemCount() + + exceptionLabel()); + } + boolean fromScrapOrHiddenOrCache = false; + ViewHolder holder = null; + // 0) If there is a changed scrap, try to find from there + if (mState.isPreLayout()) { + holder = getChangedScrapViewForPosition(position); + fromScrapOrHiddenOrCache = holder != null; + } + // 1) Find by position from scrap/hidden list/cache + if (holder == null) { + holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); + if (holder != null) { + if (!validateViewHolderForOffsetPosition(holder)) { + // recycle holder (and unscrap if relevant) since it can't be used + if (!dryRun) { + // we would like to recycle this but need to make sure it is not used by + // animation logic etc. + holder.addFlags(ViewHolder.FLAG_INVALID); + if (holder.isScrap()) { + removeDetachedView(holder.itemView, false); + holder.unScrap(); + } else if (holder.wasReturnedFromScrap()) { + holder.clearReturnedFromScrapFlag(); + } + recycleViewHolderInternal(holder); + } + holder = null; + } else { + fromScrapOrHiddenOrCache = true; + } + } + } + if (holder == null) { + final int offsetPosition = mAdapterHelper.findPositionOffset(position); + if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { + throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " + + "position " + position + "(offset:" + offsetPosition + ")." + + "state:" + mState.getItemCount() + exceptionLabel()); + } - /** - * Called when a view created by this adapter has been detached from its window. - * - *

    Becoming detached from the window is not necessarily a permanent condition; - * the consumer of an Adapter's views may choose to cache views offscreen while they - * are not visible, attaching and detaching them as appropriate.

    - * - * @param holder Holder of the view being detached - */ - public void onViewDetachedFromWindow(@NonNull VH holder) { - } + final int type = mAdapter.getItemViewType(offsetPosition); + // 2) Find from scrap/cache via stable ids, if exists + if (mAdapter.hasStableIds()) { + holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), + type, dryRun); + if (holder != null) { + // update position + holder.mPosition = offsetPosition; + fromScrapOrHiddenOrCache = true; + } + } + if (holder == null && mViewCacheExtension != null) { + // We are NOT sending the offsetPosition because LayoutManager does not + // know it. + final View view = mViewCacheExtension + .getViewForPositionAndType(this, position, type); + if (view != null) { + holder = getChildViewHolder(view); + if (holder == null) { + throw new IllegalArgumentException("getViewForPositionAndType returned" + + " a view which does not have a ViewHolder" + + exceptionLabel()); + } else if (holder.shouldIgnore()) { + throw new IllegalArgumentException("getViewForPositionAndType returned" + + " a view that is ignored. You must call stopIgnoring before" + + " returning this view." + exceptionLabel()); + } + } + } + if (holder == null) { // fallback to pool + if (DEBUG) { + Log.d(TAG, "tryGetViewHolderForPositionByDeadline(" + + position + ") fetching from shared pool"); + } + holder = getRecycledViewPool().getRecycledView(type); + if (holder != null) { + holder.resetInternal(); + if (FORCE_INVALIDATE_DISPLAY_LIST) { + invalidateDisplayListInt(holder); + } + } + } + if (holder == null) { + long start = getNanoTime(); + if (deadlineNs != FOREVER_NS + && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) { + // abort - we have a deadline we can't meet + return null; + } + holder = mAdapter.createViewHolder(RecyclerView.this, type); + if (ALLOW_THREAD_GAP_WORK) { + // only bother finding nested RV if prefetching + RecyclerView innerView = findNestedRecyclerView(holder.itemView); + if (innerView != null) { + holder.mNestedRecyclerView = new WeakReference<>(innerView); + } + } - /** - * Returns true if one or more observers are attached to this adapter. - * - * @return true if this adapter has observers - */ - public final boolean hasObservers() { - return mObservable.hasObservers(); - } + long end = getNanoTime(); + mRecyclerPool.factorInCreateTime(type, end - start); + if (DEBUG) { + Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder"); + } + } + } - /** - * Register a new observer to listen for data changes. - * - *

    The adapter may publish a variety of events describing specific changes. - * Not all adapters may support all change types and some may fall back to a generic - * {@link RecyclerView.AdapterDataObserver#onChanged() - * "something changed"} event if more specific data is not available.

    - * - *

    Components registering observers with an adapter are responsible for - * {@link #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver) - * unregistering} those observers when finished.

    - * - * @param observer Observer to register - * @see #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver) - */ - public void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) { - mObservable.registerObserver(observer); - } + // This is very ugly but the only place we can grab this information + // before the View is rebound and returned to the LayoutManager for post layout ops. + // We don't need this in pre-layout since the VH is not updated by the LM. + if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder + .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) { + holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); + if (mState.mRunSimpleAnimations) { + int changeFlags = ItemAnimator + .buildAdapterChangeFlagsForAnimations(holder); + changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; + final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState, + holder, changeFlags, holder.getUnmodifiedPayloads()); + recordAnimationInfoIfBouncedHiddenView(holder, info); + } + } - /** - * Unregister an observer currently listening for data changes. - * - *

    The unregistered observer will no longer receive events about changes - * to the adapter.

    - * - * @param observer Observer to unregister - * @see #registerAdapterDataObserver(RecyclerView.AdapterDataObserver) - */ - public void unregisterAdapterDataObserver(@NonNull AdapterDataObserver observer) { - mObservable.unregisterObserver(observer); - } + boolean bound = false; + if (mState.isPreLayout() && holder.isBound()) { + // do not update unless we absolutely have to. + holder.mPreLayoutPosition = position; + } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { + if (DEBUG && holder.isRemoved()) { + throw new IllegalStateException("Removed holder should be bound and it should" + + " come here only in pre-layout. Holder: " + holder + + exceptionLabel()); + } + final int offsetPosition = mAdapterHelper.findPositionOffset(position); + bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); + } - /** - * Called by RecyclerView when it starts observing this Adapter. - *

    - * Keep in mind that same adapter may be observed by multiple RecyclerViews. - * - * @param recyclerView The RecyclerView instance which started observing this adapter. - * @see #onDetachedFromRecyclerView(RecyclerView) - */ - public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); + final LayoutParams rvLayoutParams; + if (lp == null) { + rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); + holder.itemView.setLayoutParams(rvLayoutParams); + } else if (!checkLayoutParams(lp)) { + rvLayoutParams = (LayoutParams) generateLayoutParams(lp); + holder.itemView.setLayoutParams(rvLayoutParams); + } else { + rvLayoutParams = (LayoutParams) lp; + } + rvLayoutParams.mViewHolder = holder; + rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound; + return holder; } - /** - * Called by RecyclerView when it stops observing this Adapter. - * - * @param recyclerView The RecyclerView instance which stopped observing this adapter. - * @see #onAttachedToRecyclerView(RecyclerView) - */ - public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + private void attachAccessibilityDelegateOnBind(ViewHolder holder) { + if (isAccessibilityEnabled()) { + final View itemView = holder.itemView; + if (ViewCompat.getImportantForAccessibility(itemView) + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + ViewCompat.setImportantForAccessibility(itemView, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + if (mAccessibilityDelegate == null) { + return; + } + AccessibilityDelegateCompat itemDelegate = mAccessibilityDelegate.getItemDelegate(); + if (itemDelegate instanceof RecyclerViewAccessibilityDelegate.ItemDelegate) { + // If there was already an a11y delegate set on the itemView, store it in the + // itemDelegate and then set the itemDelegate as the a11y delegate. + ((RecyclerViewAccessibilityDelegate.ItemDelegate) itemDelegate) + .saveOriginalDelegate(itemView); + } + ViewCompat.setAccessibilityDelegate(itemView, itemDelegate); + } } - /** - * Notify any registered observers that the data set has changed. - * - *

    There are two different classes of data change events, item changes and structural - * changes. Item changes are when a single item has its data updated but no positional - * changes have occurred. Structural changes are when items are inserted, removed or moved - * within the data set.

    - * - *

    This event does not specify what about the data set has changed, forcing - * any observers to assume that all existing items and structure may no longer be valid. - * LayoutManagers will be forced to fully rebind and relayout all visible views.

    - * - *

    RecyclerView will attempt to synthesize visible structural change events - * for adapters that report that they have {@link #hasStableIds() stable IDs} when - * this method is used. This can help for the purposes of animation and visual - * object persistence but individual item views will still need to be rebound - * and relaid out.

    - * - *

    If you are writing an adapter it will always be more efficient to use the more - * specific change events if you can. Rely on notifyDataSetChanged() - * as a last resort.

    - * - * @see #notifyItemChanged(int) - * @see #notifyItemInserted(int) - * @see #notifyItemRemoved(int) - * @see #notifyItemRangeChanged(int, int) - * @see #notifyItemRangeInserted(int, int) - * @see #notifyItemRangeRemoved(int, int) - */ - public final void notifyDataSetChanged() { - mObservable.notifyChanged(); + private void invalidateDisplayListInt(ViewHolder holder) { + if (holder.itemView instanceof ViewGroup) { + invalidateDisplayListInt((ViewGroup) holder.itemView, false); + } } - /** - * Notify any registered observers that the item at position has changed. - * Equivalent to calling notifyItemChanged(position, null);. - * - *

    This is an item change event, not a structural change event. It indicates that any - * reflection of the data at position is out of date and should be updated. - * The item at position retains the same identity.

    - * - * @param position Position of the item that has changed - * @see #notifyItemRangeChanged(int, int) - */ - public final void notifyItemChanged(int position) { - mObservable.notifyItemRangeChanged(position, 1); + private void invalidateDisplayListInt(ViewGroup viewGroup, boolean invalidateThis) { + for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) { + final View view = viewGroup.getChildAt(i); + if (view instanceof ViewGroup) { + invalidateDisplayListInt((ViewGroup) view, true); + } + } + if (!invalidateThis) { + return; + } + // we need to force it to become invisible + if (viewGroup.getVisibility() == View.INVISIBLE) { + viewGroup.setVisibility(View.VISIBLE); + viewGroup.setVisibility(View.INVISIBLE); + } else { + final int visibility = viewGroup.getVisibility(); + viewGroup.setVisibility(View.INVISIBLE); + viewGroup.setVisibility(visibility); + } } /** - * Notify any registered observers that the item at position has changed with - * an optional payload object. - * - *

    This is an item change event, not a structural change event. It indicates that any - * reflection of the data at position is out of date and should be updated. - * The item at position retains the same identity. - *

    + * Recycle a detached view. The specified view will be added to a pool of views + * for later rebinding and reuse. * - *

    - * Client can optionally pass a payload for partial change. These payloads will be merged - * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the - * item is already represented by a ViewHolder and it will be rebound to the same - * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing - * payloads on that item and prevent future payload until - * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume - * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not - * attached, the payload will be simply dropped. + *

    A view must be fully detached (removed from parent) before it may be recycled. If the + * View is scrapped, it will be removed from scrap list.

    * - * @param position Position of the item that has changed - * @param payload Optional parameter, use null to identify a "full" update - * @see #notifyItemRangeChanged(int, int) + * @param view Removed view for recycling + * @see LayoutManager#removeAndRecycleView(View, Recycler) */ - public final void notifyItemChanged(int position, @Nullable Object payload) { - mObservable.notifyItemRangeChanged(position, 1, payload); + public void recycleView(@NonNull View view) { + // This public recycle method tries to make view recycle-able since layout manager + // intended to recycle this view (e.g. even if it is in scrap or change cache) + ViewHolder holder = getChildViewHolderInt(view); + if (holder.isTmpDetached()) { + removeDetachedView(view, false); + } + if (holder.isScrap()) { + holder.unScrap(); + } else if (holder.wasReturnedFromScrap()) { + holder.clearReturnedFromScrapFlag(); + } + recycleViewHolderInternal(holder); + // In most cases we dont need call endAnimation() because when view is detached, + // ViewPropertyAnimation will end. But if the animation is based on ObjectAnimator or + // if the ItemAnimator uses "pending runnable" and the ViewPropertyAnimation has not + // started yet, the ItemAnimatior on the view may not be cleared. + // In b/73552923, the View is removed by scroll pass while it's waiting in + // the "pending moving" list of DefaultItemAnimator and DefaultItemAnimator later in + // a post runnable, incorrectly performs postDelayed() on the detached view. + // To fix the issue, we issue endAnimation() here to make sure animation of this view + // finishes. + // + // Note the order: we must call endAnimation() after recycleViewHolderInternal() + // to avoid recycle twice. If ViewHolder isRecyclable is false, + // recycleViewHolderInternal() will not recycle it, endAnimation() will reset + // isRecyclable flag and recycle the view. + if (mItemAnimator != null && !holder.isRecyclable()) { + mItemAnimator.endAnimation(holder); + } } - /** - * Notify any registered observers that the itemCount items starting at - * position positionStart have changed. - * Equivalent to calling notifyItemRangeChanged(position, itemCount, null);. - * - *

    This is an item change event, not a structural change event. It indicates that - * any reflection of the data in the given position range is out of date and should - * be updated. The items in the given range retain the same identity.

    - * - * @param positionStart Position of the first item that has changed - * @param itemCount Number of items that have changed - * @see #notifyItemChanged(int) - */ - public final void notifyItemRangeChanged(int positionStart, int itemCount) { - mObservable.notifyItemRangeChanged(positionStart, itemCount); + void recycleAndClearCachedViews() { + final int count = mCachedViews.size(); + for (int i = count - 1; i >= 0; i--) { + recycleCachedViewAt(i); + } + mCachedViews.clear(); + if (ALLOW_THREAD_GAP_WORK) { + mPrefetchRegistry.clearPrefetchPositions(); + } } /** - * Notify any registered observers that the itemCount items starting at - * position positionStart have changed. An optional payload can be - * passed to each changed item. - * - *

    This is an item change event, not a structural change event. It indicates that any - * reflection of the data in the given position range is out of date and should be updated. - * The items in the given range retain the same identity. - *

    - * + * Recycles a cached view and removes the view from the list. Views are added to cache + * if and only if they are recyclable, so this method does not check it again. *

    - * Client can optionally pass a payload for partial change. These payloads will be merged - * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the - * item is already represented by a ViewHolder and it will be rebound to the same - * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing - * payloads on that item and prevent future payload until - * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume - * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not - * attached, the payload will be simply dropped. + * A small exception to this rule is when the view does not have an animator reference + * but transient state is true (due to animations created outside ItemAnimator). In that + * case, adapter may choose to recycle it. From RecyclerView's perspective, the view is + * still recyclable since Adapter wants to do so. * - * @param positionStart Position of the first item that has changed - * @param itemCount Number of items that have changed - * @param payload Optional parameter, use null to identify a "full" update - * @see #notifyItemChanged(int) + * @param cachedViewIndex The index of the view in cached views list */ - public final void notifyItemRangeChanged(int positionStart, int itemCount, - @Nullable Object payload) { - mObservable.notifyItemRangeChanged(positionStart, itemCount, payload); + void recycleCachedViewAt(int cachedViewIndex) { + if (DEBUG) { + Log.d(TAG, "Recycling cached view at index " + cachedViewIndex); + } + ViewHolder viewHolder = mCachedViews.get(cachedViewIndex); + if (DEBUG) { + Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder); + } + addViewHolderToRecycledViewPool(viewHolder, true); + mCachedViews.remove(cachedViewIndex); } /** - * Notify any registered observers that the item reflected at position - * has been newly inserted. The item previously at position is now at - * position position + 1. - * - *

    This is a structural change event. Representations of other existing items in the - * data set are still considered up to date and will not be rebound, though their - * positions may be altered.

    - * - * @param position Position of the newly inserted item in the data set - * @see #notifyItemRangeInserted(int, int) + * internal implementation checks if view is scrapped or attached and throws an exception + * if so. + * Public version un-scraps before calling recycle. */ - public final void notifyItemInserted(int position) { - mObservable.notifyItemRangeInserted(position, 1); - } + void recycleViewHolderInternal(ViewHolder holder) { + if (holder.isScrap() || holder.itemView.getParent() != null) { + throw new IllegalArgumentException( + "Scrapped or attached views may not be recycled. isScrap:" + + holder.isScrap() + " isAttached:" + + (holder.itemView.getParent() != null) + exceptionLabel()); + } - /** - * Notify any registered observers that the item reflected at fromPosition - * has been moved to toPosition. - * - *

    This is a structural change event. Representations of other existing items in the - * data set are still considered up to date and will not be rebound, though their - * positions may be altered.

    - * - * @param fromPosition Previous position of the item. - * @param toPosition New position of the item. - */ - public final void notifyItemMoved(int fromPosition, int toPosition) { - mObservable.notifyItemMoved(fromPosition, toPosition); - } + if (holder.isTmpDetached()) { + throw new IllegalArgumentException("Tmp detached view should be removed " + + "from RecyclerView before it can be recycled: " + holder + + exceptionLabel()); + } + + if (holder.shouldIgnore()) { + throw new IllegalArgumentException("Trying to recycle an ignored view holder. You" + + " should first call stopIgnoringView(view) before calling recycle." + + exceptionLabel()); + } + final boolean transientStatePreventsRecycling = holder + .doesTransientStatePreventRecycling(); + @SuppressWarnings("unchecked") final boolean forceRecycle = mAdapter != null + && transientStatePreventsRecycling + && mAdapter.onFailedToRecycleView(holder); + boolean cached = false; + boolean recycled = false; + if (DEBUG && mCachedViews.contains(holder)) { + throw new IllegalArgumentException("cached view received recycle internal? " + + holder + exceptionLabel()); + } + if (forceRecycle || holder.isRecyclable()) { + if (mViewCacheMax > 0 + && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID + | ViewHolder.FLAG_REMOVED + | ViewHolder.FLAG_UPDATE + | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { + // Retire oldest cached view + int cachedViewSize = mCachedViews.size(); + if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { + recycleCachedViewAt(0); + cachedViewSize--; + } - /** - * Notify any registered observers that the currently reflected itemCount - * items starting at positionStart have been newly inserted. The items - * previously located at positionStart and beyond can now be found starting - * at position positionStart + itemCount. - * - *

    This is a structural change event. Representations of other existing items in the - * data set are still considered up to date and will not be rebound, though their positions - * may be altered.

    - * - * @param positionStart Position of the first item that was inserted - * @param itemCount Number of items inserted - * @see #notifyItemInserted(int) - */ - public final void notifyItemRangeInserted(int positionStart, int itemCount) { - mObservable.notifyItemRangeInserted(positionStart, itemCount); - } + int targetCacheIndex = cachedViewSize; + if (ALLOW_THREAD_GAP_WORK + && cachedViewSize > 0 + && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) { + // when adding the view, skip past most recently prefetched views + int cacheIndex = cachedViewSize - 1; + while (cacheIndex >= 0) { + int cachedPos = mCachedViews.get(cacheIndex).mPosition; + if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) { + break; + } + cacheIndex--; + } + targetCacheIndex = cacheIndex + 1; + } + mCachedViews.add(targetCacheIndex, holder); + cached = true; + } + if (!cached) { + addViewHolderToRecycledViewPool(holder, true); + recycled = true; + } + } else { + // NOTE: A view can fail to be recycled when it is scrolled off while an animation + // runs. In this case, the item is eventually recycled by + // ItemAnimatorRestoreListener#onAnimationFinished. - /** - * Notify any registered observers that the item previously located at position - * has been removed from the data set. The items previously located at and after - * position may now be found at oldPosition - 1. - * - *

    This is a structural change event. Representations of other existing items in the - * data set are still considered up to date and will not be rebound, though their positions - * may be altered.

    - * - * @param position Position of the item that has now been removed - * @see #notifyItemRangeRemoved(int, int) - */ - public final void notifyItemRemoved(int position) { - mObservable.notifyItemRangeRemoved(position, 1); + // TODO: consider cancelling an animation when an item is removed scrollBy, + // to return it to the pool faster + if (DEBUG) { + Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will " + + "re-visit here. We are still removing it from animation lists" + + exceptionLabel()); + } + } + // even if the holder is not removed, we still call this method so that it is removed + // from view holder lists. + mViewInfoStore.removeViewHolder(holder); + if (!cached && !recycled && transientStatePreventsRecycling) { + PoolingContainer.callPoolingContainerOnRelease(holder.itemView); + holder.mBindingAdapter = null; + holder.mOwnerRecyclerView = null; + } } /** - * Notify any registered observers that the itemCount items previously - * located at positionStart have been removed from the data set. The items - * previously located at and after positionStart + itemCount may now be found - * at oldPosition - itemCount. + * Prepares the ViewHolder to be removed/recycled, and inserts it into the RecycledViewPool. * - *

    This is a structural change event. Representations of other existing items in the data - * set are still considered up to date and will not be rebound, though their positions - * may be altered.

    + * Pass false to dispatchRecycled for views that have not been bound. * - * @param positionStart Previous position of the first item that was removed - * @param itemCount Number of items removed from the data set + * @param holder Holder to be added to the pool. + * @param dispatchRecycled True to dispatch View recycled callbacks. */ - public final void notifyItemRangeRemoved(int positionStart, int itemCount) { - mObservable.notifyItemRangeRemoved(positionStart, itemCount); + void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) { + clearNestedRecyclerViewIfNotNested(holder); + View itemView = holder.itemView; + if (mAccessibilityDelegate != null) { + AccessibilityDelegateCompat itemDelegate = mAccessibilityDelegate.getItemDelegate(); + AccessibilityDelegateCompat originalDelegate = null; + if (itemDelegate instanceof RecyclerViewAccessibilityDelegate.ItemDelegate) { + originalDelegate = + ((RecyclerViewAccessibilityDelegate.ItemDelegate) itemDelegate) + .getAndRemoveOriginalDelegateForItem(itemView); + } + // Set the a11y delegate back to whatever the original delegate was. + ViewCompat.setAccessibilityDelegate(itemView, originalDelegate); + } + if (dispatchRecycled) { + dispatchViewRecycled(holder); + } + holder.mBindingAdapter = null; + holder.mOwnerRecyclerView = null; + getRecycledViewPool().putRecycledView(holder); } /** - * Returns when this Adapter wants to restore the state. - * - * @return The current {@link StateRestorationPolicy} for this Adapter. Defaults to - * {@link StateRestorationPolicy#ALLOW}. - * @see #setStateRestorationPolicy(StateRestorationPolicy) + * Used as a fast path for unscrapping and recycling a view during a bulk operation. + * The caller must call {@link #clearScrap()} when it's done to update the recycler's + * internal bookkeeping. */ - @NonNull - public final StateRestorationPolicy getStateRestorationPolicy() { - return mStateRestorationPolicy; + void quickRecycleScrapView(View view) { + final ViewHolder holder = getChildViewHolderInt(view); + holder.mScrapContainer = null; + holder.mInChangeScrap = false; + holder.clearReturnedFromScrapFlag(); + recycleViewHolderInternal(holder); } /** - * Sets the state restoration strategy for the Adapter. - *

    - * By default, it is set to {@link StateRestorationPolicy#ALLOW} which means RecyclerView - * expects any set Adapter to be immediately capable of restoring the RecyclerView's saved - * scroll position. - *

    - * This behaviour might be undesired if the Adapter's data is loaded asynchronously, and - * thus unavailable during initial layout (e.g. after Activity rotation). To avoid losing - * scroll position, you can change this to be either - * {@link StateRestorationPolicy#PREVENT_WHEN_EMPTY} or - * {@link StateRestorationPolicy#PREVENT}. - * Note that the former means your RecyclerView will restore state as soon as Adapter has - * 1 or more items while the latter requires you to call - * {@link #setStateRestorationPolicy(StateRestorationPolicy)} with either - * {@link StateRestorationPolicy#ALLOW} or - * {@link StateRestorationPolicy#PREVENT_WHEN_EMPTY} again when the Adapter is - * ready to restore its state. - *

    - * RecyclerView will still layout even when State restoration is disabled. The behavior of - * how State is restored is up to the {@link LayoutManager}. All default LayoutManagers - * will override current state with restored state when state restoration happens (unless - * an explicit call to {@link LayoutManager#scrollToPosition(int)} is made). - *

    - * Calling this method after state is restored will not have any effect other than changing - * the return value of {@link #getStateRestorationPolicy()}. + * Mark an attached view as scrap. * - * @param strategy The saved state restoration strategy for this Adapter. - * @see #getStateRestorationPolicy() - */ - public void setStateRestorationPolicy(@NonNull StateRestorationPolicy strategy) { - mStateRestorationPolicy = strategy; - mObservable.notifyStateRestorationPolicyChanged(); - } - - /** - * Called by the RecyclerView to decide whether the SavedState should be given to the - * LayoutManager or not. + *

    "Scrap" views are still attached to their parent RecyclerView but are eligible + * for rebinding and reuse. Requests for a view for a given position may return a + * reused or rebound scrap view instance.

    * - * @return {@code true} if the Adapter is ready to restore its state, {@code false} - * otherwise. + * @param view View to scrap */ - boolean canRestoreState() { - switch (mStateRestorationPolicy) { - case PREVENT: - return false; - case PREVENT_WHEN_EMPTY: - return getItemCount() > 0; - default: - return true; + void scrapView(View view) { + final ViewHolder holder = getChildViewHolderInt(view); + if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) + || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { + if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) { + throw new IllegalArgumentException("Called scrap view with an invalid view." + + " Invalid views cannot be reused from scrap, they should rebound from" + + " recycler pool." + exceptionLabel()); + } + holder.setScrapContainer(this, false); + mAttachedScrap.add(holder); + } else { + if (mChangedScrap == null) { + mChangedScrap = new ArrayList<>(); + } + holder.setScrapContainer(this, true); + mChangedScrap.add(holder); } } /** - * Defines how this Adapter wants to restore its state after a view reconstruction (e.g. - * configuration change). - */ - public enum StateRestorationPolicy { - /** - * Adapter is ready to restore State immediately, RecyclerView will provide the state - * to the LayoutManager in the next layout pass. - */ - ALLOW, - /** - * Adapter is ready to restore State when it has more than 0 items. RecyclerView will - * provide the state to the LayoutManager as soon as the Adapter has 1 or more items. - */ - PREVENT_WHEN_EMPTY, - /** - * RecyclerView will not restore the state for the Adapter until a call to - * {@link #setStateRestorationPolicy(StateRestorationPolicy)} is made with either - * {@link #ALLOW} or {@link #PREVENT_WHEN_EMPTY}. - */ - PREVENT - } - } - - /** - * A LayoutManager is responsible for measuring and positioning item views - * within a RecyclerView as well as determining the policy for when to recycle - * item views that are no longer visible to the user. By changing the LayoutManager - * a RecyclerView can be used to implement a standard vertically scrolling list, - * a uniform grid, staggered grids, horizontally scrolling collections and more. Several stock - * layout managers are provided for general use. - *

    - * If the LayoutManager specifies a default constructor or one with the signature - * ({@link Context}, {@link AttributeSet}, {@code int}, {@code int}), RecyclerView will - * instantiate and set the LayoutManager when being inflated. Most used properties can - * be then obtained from {@link #getProperties(Context, AttributeSet, int, int)}. In case - * a LayoutManager specifies both constructors, the non-default constructor will take - * precedence. - */ - public abstract static class LayoutManager { - ChildHelper mChildHelper; - RecyclerView mRecyclerView; - /** - * The callback used for retrieving information about a RecyclerView and its children in the - * vertical direction. - */ - private final ViewBoundsCheck.Callback mVerticalBoundCheckCallback = - new ViewBoundsCheck.Callback() { - @Override - public View getChildAt(int index) { - return LayoutManager.this.getChildAt(index); - } + * Remove a previously scrapped view from the pool of eligible scrap. + * + *

    This view will no longer be eligible for reuse until re-scrapped or + * until it is explicitly removed and recycled.

    + */ + void unscrapView(ViewHolder holder) { + if (holder.mInChangeScrap) { + mChangedScrap.remove(holder); + } else { + mAttachedScrap.remove(holder); + } + holder.mScrapContainer = null; + holder.mInChangeScrap = false; + holder.clearReturnedFromScrapFlag(); + } - @Override - public int getParentStart() { - return getPaddingTop(); - } + int getScrapCount() { + return mAttachedScrap.size(); + } - @Override - public int getParentEnd() { - return getHeight() - - getPaddingBottom(); - } + View getScrapViewAt(int index) { + return mAttachedScrap.get(index).itemView; + } - @Override - public int getChildStart(View view) { - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) - view.getLayoutParams(); - return getDecoratedTop(view) - params.topMargin; - } + void clearScrap() { + mAttachedScrap.clear(); + if (mChangedScrap != null) { + mChangedScrap.clear(); + } + } - @Override - public int getChildEnd(View view) { - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) - view.getLayoutParams(); - return getDecoratedBottom(view) + params.bottomMargin; + ViewHolder getChangedScrapViewForPosition(int position) { + // If pre-layout, check the changed scrap for an exact match. + final int changedScrapSize; + if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) { + return null; + } + // find by position + for (int i = 0; i < changedScrapSize; i++) { + final ViewHolder holder = mChangedScrap.get(i); + if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) { + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + return holder; + } + } + // find by id + if (mAdapter.hasStableIds()) { + final int offsetPosition = mAdapterHelper.findPositionOffset(position); + if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) { + final long id = mAdapter.getItemId(offsetPosition); + for (int i = 0; i < changedScrapSize; i++) { + final ViewHolder holder = mChangedScrap.get(i); + if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) { + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + return holder; + } } - }; - final ViewBoundsCheck mVerticalBoundCheck = new ViewBoundsCheck(mVerticalBoundCheckCallback); - @Nullable - SmoothScroller mSmoothScroller; - boolean mRequestedSimpleAnimations; - boolean mIsAttachedToWindow; - /** - * This field is only set via the deprecated {@link #setAutoMeasureEnabled(boolean)} and is - * only accessed via {@link #isAutoMeasureEnabled()} for backwards compatability reasons. - */ - boolean mAutoMeasure; - /** - * Written by {@link GapWorker} when prefetches occur to track largest number of view ever - * requested by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)} or - * {@link #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)} call. - *

    - * If expanded by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, - * will be reset upon layout to prevent initial prefetches (often large, since they're - * proportional to expected child count) from expanding cache permanently. - */ - int mPrefetchMaxCountObserved; - /** - * If true, mPrefetchMaxCountObserved is only valid until next layout, and should be reset. - */ - boolean mPrefetchMaxObservedInInitialPrefetch; - /** - * LayoutManager has its own more strict measurement cache to avoid re-measuring a child - * if the space that will be given to it is already larger than what it has measured before. - */ - private boolean mMeasurementCacheEnabled = true; + } + } + return null; + } - private boolean mItemPrefetchEnabled = true; - /** - * These measure specs might be the measure specs that were passed into RecyclerView's - * onMeasure method OR fake measure specs created by the RecyclerView. - * For example, when a layout is run, RecyclerView always sets these specs to be - * EXACTLY because a LayoutManager cannot resize RecyclerView during a layout pass. - *

    - * Also, to be able to use the hint in unspecified measure specs, RecyclerView checks the - * API level and sets the size to 0 pre-M to avoid any issue that might be caused by - * corrupt values. Older platforms have no responsibility to provide a size if they set - * mode to unspecified. - */ - private int mWidthMode, mHeightMode; - private int mWidth, mHeight; /** - * The callback used for retrieving information about a RecyclerView and its children in the - * horizontal direction. + * Returns a view for the position either from attach scrap, hidden children, or cache. + * + * @param position Item position + * @param dryRun Does a dry run, finds the ViewHolder but does not remove + * @return a ViewHolder that can be re-used for this position. */ - private final ViewBoundsCheck.Callback mHorizontalBoundCheckCallback = - new ViewBoundsCheck.Callback() { - @Override - public View getChildAt(int index) { - return LayoutManager.this.getChildAt(index); - } + ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { + final int scrapCount = mAttachedScrap.size(); - @Override - public int getParentStart() { - return getPaddingLeft(); + // Try first for an exact, non-invalid match from scrap. + for (int i = 0; i < scrapCount; i++) { + final ViewHolder holder = mAttachedScrap.get(i); + if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position + && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) { + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + return holder; + } + } + + if (!dryRun) { + View view = mChildHelper.findHiddenNonRemovedView(position); + if (view != null) { + // This View is good to be used. We just need to unhide, detach and move to the + // scrap list. + final ViewHolder vh = getChildViewHolderInt(view); + mChildHelper.unhide(view); + int layoutIndex = mChildHelper.indexOfChild(view); + if (layoutIndex == RecyclerView.NO_POSITION) { + throw new IllegalStateException("layout index should not be -1 after " + + "unhiding a view:" + vh + exceptionLabel()); } + mChildHelper.detachViewFromParent(layoutIndex); + scrapView(view); + vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP + | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); + return vh; + } + } - @Override - public int getParentEnd() { - return getWidth() - getPaddingRight(); + // Search in our first-level recycled view cache. + final int cacheSize = mCachedViews.size(); + for (int i = 0; i < cacheSize; i++) { + final ViewHolder holder = mCachedViews.get(i); + // invalid view holders may be in cache if adapter has stable ids as they can be + // retrieved via getScrapOrCachedViewForId + if (!holder.isInvalid() && holder.getLayoutPosition() == position + && !holder.isAttachedToTransitionOverlay()) { + if (!dryRun) { + mCachedViews.remove(i); + } + if (DEBUG) { + Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position + + ") found match in cache: " + holder); } + return holder; + } + } + return null; + } - @Override - public int getChildStart(View view) { - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) - view.getLayoutParams(); - return getDecoratedLeft(view) - params.leftMargin; + ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) { + // Look in our attached views first + final int count = mAttachedScrap.size(); + for (int i = count - 1; i >= 0; i--) { + final ViewHolder holder = mAttachedScrap.get(i); + if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) { + if (type == holder.getItemViewType()) { + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + if (holder.isRemoved()) { + // this might be valid in two cases: + // > item is removed but we are in pre-layout pass + // >> do nothing. return as is. make sure we don't rebind + // > item is removed then added to another position and we are in + // post layout. + // >> remove removed and invalid flags, add update flag to rebind + // because item was invisible to us and we don't know what happened in + // between. + if (!mState.isPreLayout()) { + holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE + | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED); + } + } + return holder; + } else if (!dryRun) { + // if we are running animations, it is actually better to keep it in scrap + // but this would force layout manager to lay it out which would be bad. + // Recycle this scrap. Type mismatch. + mAttachedScrap.remove(i); + removeDetachedView(holder.itemView, false); + quickRecycleScrapView(holder.itemView); } + } + } - @Override - public int getChildEnd(View view) { - RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) - view.getLayoutParams(); - return getDecoratedRight(view) + params.rightMargin; + // Search the first-level cache + final int cacheSize = mCachedViews.size(); + for (int i = cacheSize - 1; i >= 0; i--) { + final ViewHolder holder = mCachedViews.get(i); + if (holder.getItemId() == id && !holder.isAttachedToTransitionOverlay()) { + if (type == holder.getItemViewType()) { + if (!dryRun) { + mCachedViews.remove(i); + } + return holder; + } else if (!dryRun) { + recycleCachedViewAt(i); + return null; } - }; - /** - * Utility objects used to check the boundaries of children against their parent - * RecyclerView. - * - * @see #isViewPartiallyVisible(View, boolean, boolean), - * {@link LinearLayoutManager#findOneVisibleChild(int, int, boolean, boolean)}, - * and {@link LinearLayoutManager#findOnePartiallyOrCompletelyInvisibleChild(int, int)}. - */ - final ViewBoundsCheck mHorizontalBoundCheck = new ViewBoundsCheck(mHorizontalBoundCheckCallback); + } + } + return null; + } - /** - * Chooses a size from the given specs and parameters that is closest to the desired size - * and also complies with the spec. - * - * @param spec The measureSpec - * @param desired The preferred measurement - * @param min The minimum value - * @return A size that fits to the given specs - */ - public static int chooseSize(int spec, int desired, int min) { - int mode = View.MeasureSpec.getMode(spec); - int size = View.MeasureSpec.getSize(spec); - switch (mode) { - case View.MeasureSpec.EXACTLY: - return size; - case View.MeasureSpec.AT_MOST: - return Math.min(size, Math.max(desired, min)); - case View.MeasureSpec.UNSPECIFIED: - default: - return Math.max(desired, min); + @SuppressWarnings("unchecked") + void dispatchViewRecycled(@NonNull ViewHolder holder) { + // TODO: Remove this once setRecyclerListener (currently deprecated) is deleted. + if (mRecyclerListener != null) { + mRecyclerListener.onViewRecycled(holder); + } + + final int listenerCount = mRecyclerListeners.size(); + for (int i = 0; i < listenerCount; i++) { + mRecyclerListeners.get(i).onViewRecycled(holder); + } + if (mAdapter != null) { + mAdapter.onViewRecycled(holder); + } + if (mState != null) { + mViewInfoStore.removeViewHolder(holder); } + if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder); + } + + void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter, + boolean compatibleWithPrevious) { + clear(); + poolingContainerDetach(oldAdapter, true); + getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, + compatibleWithPrevious); + maybeSendPoolingContainerAttach(); } - private static boolean isMeasurementUpToDate(int childSize, int spec, int dimension) { - int specMode = MeasureSpec.getMode(spec); - int specSize = MeasureSpec.getSize(spec); - if (dimension > 0 && childSize != dimension) { - return false; + void offsetPositionRecordsForMove(int from, int to) { + final int start, end, inBetweenOffset; + if (from < to) { + start = from; + end = to; + inBetweenOffset = -1; + } else { + start = to; + end = from; + inBetweenOffset = 1; } - switch (specMode) { - case MeasureSpec.UNSPECIFIED: - return true; - case MeasureSpec.AT_MOST: - return specSize >= childSize; - case MeasureSpec.EXACTLY: - return specSize == childSize; + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + if (holder == null || holder.mPosition < start || holder.mPosition > end) { + continue; + } + if (holder.mPosition == from) { + holder.offsetPosition(to - from, false); + } else { + holder.offsetPosition(inBetweenOffset, false); + } + if (DEBUG) { + Log.d(TAG, "offsetPositionRecordsForMove cached child " + i + " holder " + + holder); + } } - return false; } - /** - * Calculate a MeasureSpec value for measuring a child view in one dimension. - * - * @param parentSize Size of the parent view where the child will be placed - * @param padding Total space currently consumed by other elements of the parent - * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT. - * Generally obtained from the child view's LayoutParams - * @param canScroll true if the parent RecyclerView can scroll in this dimension - * @return a MeasureSpec value for the child view - * @deprecated use {@link #getChildMeasureSpec(int, int, int, int, boolean)} - */ - @Deprecated - public static int getChildMeasureSpec(int parentSize, int padding, int childDimension, - boolean canScroll) { - int size = Math.max(0, parentSize - padding); - int resultSize = 0; - int resultMode = 0; - if (canScroll) { - if (childDimension >= 0) { - resultSize = childDimension; - resultMode = MeasureSpec.EXACTLY; - } else { - // MATCH_PARENT can't be applied since we can scroll in this dimension, wrap - // instead using UNSPECIFIED. - resultSize = 0; - resultMode = MeasureSpec.UNSPECIFIED; - } - } else { - if (childDimension >= 0) { - resultSize = childDimension; - resultMode = MeasureSpec.EXACTLY; - } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) { - resultSize = size; - // TODO this should be my spec. - resultMode = MeasureSpec.EXACTLY; - } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) { - resultSize = size; - resultMode = MeasureSpec.AT_MOST; + void offsetPositionRecordsForInsert(int insertedAt, int count) { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + if (holder != null && holder.mPosition >= insertedAt) { + if (DEBUG) { + Log.d(TAG, "offsetPositionRecordsForInsert cached " + i + " holder " + + holder + " now at position " + (holder.mPosition + count)); + } + // insertions only affect post layout hence don't apply them to pre-layout. + holder.offsetPosition(count, false); } } - return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } /** - * Calculate a MeasureSpec value for measuring a child view in one dimension. - * - * @param parentSize Size of the parent view where the child will be placed - * @param parentMode The measurement spec mode of the parent - * @param padding Total space currently consumed by other elements of parent - * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT. - * Generally obtained from the child view's LayoutParams - * @param canScroll true if the parent RecyclerView can scroll in this dimension - * @return a MeasureSpec value for the child view + * @param removedFrom Remove start index + * @param count Remove count + * @param applyToPreLayout If true, changes will affect ViewHolder's pre-layout position, if + * false, they'll be applied before the second layout pass */ - public static int getChildMeasureSpec(int parentSize, int parentMode, int padding, - int childDimension, boolean canScroll) { - int size = Math.max(0, parentSize - padding); - int resultSize = 0; - int resultMode = 0; - if (canScroll) { - if (childDimension >= 0) { - resultSize = childDimension; - resultMode = MeasureSpec.EXACTLY; - } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) { - switch (parentMode) { - case MeasureSpec.AT_MOST: - case MeasureSpec.EXACTLY: - resultSize = size; - resultMode = parentMode; - break; - case MeasureSpec.UNSPECIFIED: - resultSize = 0; - resultMode = MeasureSpec.UNSPECIFIED; - break; - } - } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) { - resultSize = 0; - resultMode = MeasureSpec.UNSPECIFIED; - } - } else { - if (childDimension >= 0) { - resultSize = childDimension; - resultMode = MeasureSpec.EXACTLY; - } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) { - resultSize = size; - resultMode = parentMode; - } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) { - resultSize = size; - if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) { - resultMode = MeasureSpec.AT_MOST; - } else { - resultMode = MeasureSpec.UNSPECIFIED; + void offsetPositionRecordsForRemove(int removedFrom, int count, boolean applyToPreLayout) { + final int removedEnd = removedFrom + count; + final int cachedCount = mCachedViews.size(); + for (int i = cachedCount - 1; i >= 0; i--) { + final ViewHolder holder = mCachedViews.get(i); + if (holder != null) { + if (holder.mPosition >= removedEnd) { + if (DEBUG) { + Log.d(TAG, "offsetPositionRecordsForRemove cached " + i + + " holder " + holder + " now at position " + + (holder.mPosition - count)); + } + holder.offsetPosition(-count, applyToPreLayout); + } else if (holder.mPosition >= removedFrom) { + // Item for this view was removed. Dump it from the cache. + holder.addFlags(ViewHolder.FLAG_REMOVED); + recycleCachedViewAt(i); } - } } - //noinspection WrongConstant - return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } - /** - * Parse the xml attributes to get the most common properties used by layout managers. - *

    - * {@link android.R.attr#orientation} - * {@link androidx.recyclerview.R.attr#spanCount} - * {@link androidx.recyclerview.R.attr#reverseLayout} - * {@link androidx.recyclerview.R.attr#stackFromEnd} - * - * @return an object containing the properties as specified in the attrs. - */ - public static Properties getProperties(@NonNull Context context, - @Nullable AttributeSet attrs, - int defStyleAttr, int defStyleRes) { - Properties properties = new Properties(); - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView, - defStyleAttr, defStyleRes); - properties.orientation = a.getInt(R.styleable.RecyclerView_android_orientation, - DEFAULT_ORIENTATION); - properties.spanCount = a.getInt(R.styleable.RecyclerView_spanCount, 1); - properties.reverseLayout = a.getBoolean(R.styleable.RecyclerView_reverseLayout, false); - properties.stackFromEnd = a.getBoolean(R.styleable.RecyclerView_stackFromEnd, false); - a.recycle(); - return properties; + void setViewCacheExtension(ViewCacheExtension extension) { + mViewCacheExtension = extension; } - void setRecyclerView(RecyclerView recyclerView) { - if (recyclerView == null) { - mRecyclerView = null; - mChildHelper = null; - mWidth = 0; - mHeight = 0; - } else { - mRecyclerView = recyclerView; - mChildHelper = recyclerView.mChildHelper; - mWidth = recyclerView.getWidth(); - mHeight = recyclerView.getHeight(); + void setRecycledViewPool(RecycledViewPool pool) { + poolingContainerDetach(mAdapter); + if (mRecyclerPool != null) { + mRecyclerPool.detach(); } - mWidthMode = MeasureSpec.EXACTLY; - mHeightMode = MeasureSpec.EXACTLY; + mRecyclerPool = pool; + if (mRecyclerPool != null && getAdapter() != null) { + mRecyclerPool.attach(); + } + maybeSendPoolingContainerAttach(); } - void setMeasureSpecs(int wSpec, int hSpec) { - mWidth = MeasureSpec.getSize(wSpec); - mWidthMode = MeasureSpec.getMode(wSpec); - if (mWidthMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) { - mWidth = 0; + private void maybeSendPoolingContainerAttach() { + if (mRecyclerPool != null + && mAdapter != null + && isAttachedToWindow()) { + mRecyclerPool.attachForPoolingContainer(mAdapter); } + } - mHeight = MeasureSpec.getSize(hSpec); - mHeightMode = MeasureSpec.getMode(hSpec); - if (mHeightMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) { - mHeight = 0; + private void poolingContainerDetach(Adapter adapter) { + poolingContainerDetach(adapter, false); + } + + private void poolingContainerDetach(Adapter adapter, boolean isBeingReplaced) { + if (mRecyclerPool != null) { + mRecyclerPool.detachForPoolingContainer(adapter, isBeingReplaced); } } - /** - * Called after a layout is calculated during a measure pass when using auto-measure. - *

    - * It simply traverses all children to calculate a bounding box then calls - * {@link #setMeasuredDimension(Rect, int, int)}. LayoutManagers can override that method - * if they need to handle the bounding box differently. - *

    - * For example, GridLayoutManager override that method to ensure that even if a column is - * empty, the GridLayoutManager still measures wide enough to include it. - * - * @param widthSpec The widthSpec that was passing into RecyclerView's onMeasure - * @param heightSpec The heightSpec that was passing into RecyclerView's onMeasure - */ - void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) { - int count = getChildCount(); - if (count == 0) { - mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); - return; + void onAttachedToWindow() { + maybeSendPoolingContainerAttach(); + } + + void onDetachedFromWindow() { + for (int i = 0; i < mCachedViews.size(); i++) { + PoolingContainer.callPoolingContainerOnRelease(mCachedViews.get(i).itemView); } - int minX = Integer.MAX_VALUE; - int minY = Integer.MAX_VALUE; - int maxX = Integer.MIN_VALUE; - int maxY = Integer.MIN_VALUE; + poolingContainerDetach(mAdapter); + } - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - Rect bounds = mRecyclerView.mTempRect; - getDecoratedBoundsWithMargins(child, bounds); - if (bounds.left < minX) { - minX = bounds.left; - } - if (bounds.right > maxX) { - maxX = bounds.right; - } - if (bounds.top < minY) { - minY = bounds.top; + RecycledViewPool getRecycledViewPool() { + if (mRecyclerPool == null) { + mRecyclerPool = new RecycledViewPool(); + maybeSendPoolingContainerAttach(); + } + return mRecyclerPool; + } + + void viewRangeUpdate(int positionStart, int itemCount) { + final int positionEnd = positionStart + itemCount; + final int cachedCount = mCachedViews.size(); + for (int i = cachedCount - 1; i >= 0; i--) { + final ViewHolder holder = mCachedViews.get(i); + if (holder == null) { + continue; } - if (bounds.bottom > maxY) { - maxY = bounds.bottom; + + final int pos = holder.mPosition; + if (pos >= positionStart && pos < positionEnd) { + holder.addFlags(ViewHolder.FLAG_UPDATE); + recycleCachedViewAt(i); + // cached views should not be flagged as changed because this will cause them + // to animate when they are returned from cache. } } - mRecyclerView.mTempRect.set(minX, minY, maxX, maxY); - setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec); } - /** - * Sets the measured dimensions from the given bounding box of the children and the - * measurement specs that were passed into {@link RecyclerView#onMeasure(int, int)}. It is - * only called if a LayoutManager returns true from - * {@link #isAutoMeasureEnabled()} and it is called after the RecyclerView calls - * {@link LayoutManager#onLayoutChildren(Recycler, State)} in the execution of - * {@link RecyclerView#onMeasure(int, int)}. - *

    - * This method must call {@link #setMeasuredDimension(int, int)}. - *

    - * The default implementation adds the RecyclerView's padding to the given bounding box - * then caps the value to be within the given measurement specs. - * - * @param childrenBounds The bounding box of all children - * @param wSpec The widthMeasureSpec that was passed into the RecyclerView. - * @param hSpec The heightMeasureSpec that was passed into the RecyclerView. - * @see #isAutoMeasureEnabled() - * @see #setMeasuredDimension(int, int) - */ - public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { - int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight(); - int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom(); - int width = chooseSize(wSpec, usedWidth, getMinimumWidth()); - int height = chooseSize(hSpec, usedHeight, getMinimumHeight()); - setMeasuredDimension(width, height); - } + void markKnownViewsInvalid() { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + if (holder != null) { + holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID); + holder.addChangePayload(null); + } + } - /** - * Calls {@code RecyclerView#requestLayout} on the underlying RecyclerView - */ - public void requestLayout() { - if (mRecyclerView != null) { - mRecyclerView.requestLayout(); + if (mAdapter == null || !mAdapter.hasStableIds()) { + // we cannot re-use cached views in this case. Recycle them all + recycleAndClearCachedViews(); } } - /** - * Checks if RecyclerView is in the middle of a layout or scroll and throws an - * {@link IllegalStateException} if it is not. - * - * @param message The message for the exception. Can be null. - * @see #assertNotInLayoutOrScroll(String) - */ - public void assertInLayoutOrScroll(String message) { - if (mRecyclerView != null) { - mRecyclerView.assertInLayoutOrScroll(message); + void clearOldPositions() { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + holder.clearOldPosition(); + } + final int scrapCount = mAttachedScrap.size(); + for (int i = 0; i < scrapCount; i++) { + mAttachedScrap.get(i).clearOldPosition(); + } + if (mChangedScrap != null) { + final int changedScrapCount = mChangedScrap.size(); + for (int i = 0; i < changedScrapCount; i++) { + mChangedScrap.get(i).clearOldPosition(); + } } } - /** - * Checks if RecyclerView is in the middle of a layout or scroll and throws an - * {@link IllegalStateException} if it is. - * - * @param message The message for the exception. Can be null. - * @see #assertInLayoutOrScroll(String) - */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void assertNotInLayoutOrScroll(String message) { - if (mRecyclerView != null) { - mRecyclerView.assertNotInLayoutOrScroll(message); + void markItemDecorInsetsDirty() { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams(); + if (layoutParams != null) { + layoutParams.mInsetsDirty = true; + } } } + } + + /** + * ViewCacheExtension is a helper class to provide an additional layer of view caching that can + * be controlled by the developer. + *

    + * When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and + * first level cache to find a matching View. If it cannot find a suitable View, Recycler will + * call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking + * {@link RecycledViewPool}. + *

    + * Note that, Recycler never sends Views to this method to be cached. It is developers + * responsibility to decide whether they want to keep their Views in this custom cache or let + * the default recycling policy handle it. + */ + public abstract static class ViewCacheExtension { /** - * Returns whether the measuring pass of layout should use the AutoMeasure mechanism of - * {@link RecyclerView} or if it should be done by the LayoutManager's implementation of - * {@link LayoutManager#onMeasure(Recycler, State, int, int)}. - *

    - * This method returns false by default (it actually returns the value passed to the - * deprecated {@link #setAutoMeasureEnabled(boolean)}) and should be overridden to return - * true if a LayoutManager wants to be auto measured by the RecyclerView. + * Returns a View that can be binded to the given Adapter position. *

    - * If this method is overridden to return true, - * {@link LayoutManager#onMeasure(Recycler, State, int, int)} should not be overridden. + * This method should not create a new View. Instead, it is expected to return + * an already created View that can be re-used for the given type and position. + * If the View is marked as ignored, it should first call + * {@link LayoutManager#stopIgnoringView(View)} before returning the View. *

    - * AutoMeasure is a RecyclerView mechanism that handles the measuring pass of layout in a - * simple and contract satisfying way, including the wrapping of children laid out by - * LayoutManager. Simply put, it handles wrapping children by calling - * {@link LayoutManager#onLayoutChildren(Recycler, State)} during a call to - * {@link RecyclerView#onMeasure(int, int)}, and then calculating desired dimensions based - * on children's dimensions and positions. It does this while supporting all existing - * animation capabilities of the RecyclerView. + * RecyclerView will re-bind the returned View to the position if necessary. + * + * @param recycler The Recycler that can be used to bind the View + * @param position The adapter position + * @param type The type of the View, defined by adapter + * @return A View that is bound to the given position or NULL if there is no View to re-use + * @see LayoutManager#ignoreView(View) + */ + @Nullable + public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, + int type); + } + + /** + * Base class for an Adapter + * + *

    Adapters provide a binding from an app-specific data set to views that are displayed + * within a {@link RecyclerView}.

    + * + * @param A class that extends ViewHolder that will be used by the adapter. + */ + public abstract static class Adapter { + private final AdapterDataObservable mObservable = new AdapterDataObservable(); + private boolean mHasStableIds = false; + private StateRestorationPolicy mStateRestorationPolicy = StateRestorationPolicy.ALLOW; + + /** + * Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent + * an item. *

    - * More specifically: - *

      - *
    1. When {@link RecyclerView#onMeasure(int, int)} is called, if the provided measure - * specs both have a mode of {@link View.MeasureSpec#EXACTLY}, RecyclerView will set its - * measured dimensions accordingly and return, allowing layout to continue as normal - * (Actually, RecyclerView will call - * {@link LayoutManager#onMeasure(Recycler, State, int, int)} for backwards compatibility - * reasons but it should not be overridden if AutoMeasure is being used).
    2. - *
    3. If one of the layout specs is not {@code EXACT}, the RecyclerView will start the - * layout process. It will first process all pending Adapter updates and - * then decide whether to run a predictive layout. If it decides to do so, it will first - * call {@link #onLayoutChildren(Recycler, State)} with {@link State#isPreLayout()} set to - * {@code true}. At this stage, {@link #getWidth()} and {@link #getHeight()} will still - * return the width and height of the RecyclerView as of the last layout calculation. + * This new ViewHolder should be constructed with a new View that can represent the items + * of the given type. You can either create a new View manually or inflate it from an XML + * layout file. *

      - * After handling the predictive case, RecyclerView will call - * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to - * {@code true} and {@link State#isPreLayout()} set to {@code false}. The LayoutManager can - * access the measurement specs via {@link #getHeight()}, {@link #getHeightMode()}, - * {@link #getWidth()} and {@link #getWidthMode()}.

    4. - *
    5. After the layout calculation, RecyclerView sets the measured width & height by - * calculating the bounding box for the children (+ RecyclerView's padding). The - * LayoutManagers can override {@link #setMeasuredDimension(Rect, int, int)} to choose - * different values. For instance, GridLayoutManager overrides this value to handle the case - * where if it is vertical and has 3 columns but only 2 items, it should still measure its - * width to fit 3 items, not 2.
    6. - *
    7. Any following calls to {@link RecyclerView#onMeasure(int, int)} will run - * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to - * {@code true} and {@link State#isPreLayout()} set to {@code false}. RecyclerView will - * take care of which views are actually added / removed / moved / changed for animations so - * that the LayoutManager should not worry about them and handle each - * {@link #onLayoutChildren(Recycler, State)} call as if it is the last one.
    8. - *
    9. When measure is complete and RecyclerView's - * {@link #onLayout(boolean, int, int, int, int)} method is called, RecyclerView checks - * whether it already did layout calculations during the measure pass and if so, it re-uses - * that information. It may still decide to call {@link #onLayoutChildren(Recycler, State)} - * if the last measure spec was different from the final dimensions or adapter contents - * have changed between the measure call and the layout call.
    10. - *
    11. Finally, animations are calculated and run as usual.
    12. - *
    + * The new ViewHolder will be used to display items of the adapter using + * {@link #onBindViewHolder(ViewHolder, int, List)}. Since it will be re-used to display + * different items in the data set, it is a good idea to cache references to sub views of + * the View to avoid unnecessary {@link View#findViewById(int)} calls. * - * @return True if the measuring pass of layout should use the AutoMeasure - * mechanism of {@link RecyclerView} or False if it should be done by the - * LayoutManager's implementation of - * {@link LayoutManager#onMeasure(Recycler, State, int, int)}. - * @see #setMeasuredDimension(Rect, int, int) - * @see #onMeasure(Recycler, State, int, int) + * @param parent The ViewGroup into which the new View will be added after it is bound to + * an adapter position. + * @param viewType The view type of the new View. + * @return A new ViewHolder that holds a View of the given view type. + * @see #getItemViewType(int) + * @see #onBindViewHolder(ViewHolder, int) */ - public boolean isAutoMeasureEnabled() { - return mAutoMeasure; - } + @NonNull + public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType); /** - * Defines whether the measuring pass of layout should use the AutoMeasure mechanism of - * {@link RecyclerView} or if it should be done by the LayoutManager's implementation of - * {@link LayoutManager#onMeasure(Recycler, State, int, int)}. + * Called by RecyclerView to display the data at the specified position. This method should + * update the contents of the {@link ViewHolder#itemView} to reflect the item at the given + * position. + *

    + * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method + * again if the position of the item changes in the data set unless the item itself is + * invalidated or the new position cannot be determined. For this reason, you should only + * use the position parameter while acquiring the related data item inside + * this method and should not keep a copy of it. If you need the position of an item later + * on (e.g. in a click listener), use {@link ViewHolder#getBindingAdapterPosition()} which + * will have the updated adapter position. * - * @param enabled True if layout measurement should be done by the - * RecyclerView, false if it should be done by this - * LayoutManager. - * @see #isAutoMeasureEnabled() - * @deprecated Implementors of LayoutManager should define whether or not it uses - * AutoMeasure by overriding {@link #isAutoMeasureEnabled()}. + * Override {@link #onBindViewHolder(ViewHolder, int, List)} instead if Adapter can + * handle efficient partial bind. + * + * @param holder The ViewHolder which should be updated to represent the contents of the + * item at the given position in the data set. + * @param position The position of the item within the adapter's data set. */ - @Deprecated - public void setAutoMeasureEnabled(boolean enabled) { - mAutoMeasure = enabled; - } + public abstract void onBindViewHolder(@NonNull VH holder, int position); /** - * Returns whether this LayoutManager supports "predictive item animations". + * Called by RecyclerView to display the data at the specified position. This method + * should update the contents of the {@link ViewHolder#itemView} to reflect the item at + * the given position. *

    - * "Predictive item animations" are automatically created animations that show - * where items came from, and where they are going to, as items are added, removed, - * or moved within a layout. + * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method + * again if the position of the item changes in the data set unless the item itself is + * invalidated or the new position cannot be determined. For this reason, you should only + * use the position parameter while acquiring the related data item inside + * this method and should not keep a copy of it. If you need the position of an item later + * on (e.g. in a click listener), use {@link ViewHolder#getBindingAdapterPosition()} which + * will have the updated adapter position. *

    - * A LayoutManager wishing to support predictive item animations must override this - * method to return true (the default implementation returns false) and must obey certain - * behavioral contracts outlined in {@link #onLayoutChildren(Recycler, State)}. + * Partial bind vs full bind: *

    - * Whether item animations actually occur in a RecyclerView is actually determined by both - * the return value from this method and the - * {@link RecyclerView#setItemAnimator(ItemAnimator) ItemAnimator} set on the - * RecyclerView itself. If the RecyclerView has a non-null ItemAnimator but this - * method returns false, then only "simple item animations" will be enabled in the - * RecyclerView, in which views whose position are changing are simply faded in/out. If the - * RecyclerView has a non-null ItemAnimator and this method returns true, then predictive - * item animations will be enabled in the RecyclerView. + * The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or + * {@link #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty, + * the ViewHolder is currently bound to old data and Adapter may run an efficient partial + * update using the payload info. If the payload is empty, Adapter must run a full bind. + * Adapter should not assume that the payload passed in notify methods will be received by + * onBindViewHolder(). For example when the view is not attached to the screen, the + * payload in notifyItemChange() will be simply dropped. * - * @return true if this LayoutManager supports predictive item animations, false otherwise. + * @param holder The ViewHolder which should be updated to represent the contents of the + * item at the given position in the data set. + * @param position The position of the item within the adapter's data set. + * @param payloads A non-null list of merged payloads. Can be empty list if requires full + * update. */ - public boolean supportsPredictiveItemAnimations() { - return false; + public void onBindViewHolder(@NonNull VH holder, int position, + @NonNull List payloads) { + onBindViewHolder(holder, position); } /** - * Sets whether the LayoutManager should be queried for views outside of - * its viewport while the UI thread is idle between frames. + * Returns the position of the given {@link ViewHolder} in the given {@link Adapter}. * - * @return true if item prefetch is enabled, false otherwise - * @see #setItemPrefetchEnabled(boolean) + * If the given {@link Adapter} is not part of this {@link Adapter}, + * {@link RecyclerView#NO_POSITION} is returned. + * + * @param adapter The adapter which is a sub adapter of this adapter or itself. + * @param viewHolder The ViewHolder whose local position in the given adapter will be + * returned. + * @param localPosition The position of the given {@link ViewHolder} in this + * {@link Adapter}. + * + * @return The local position of the given {@link ViewHolder} in this {@link Adapter} + * or {@link RecyclerView#NO_POSITION} if the {@link ViewHolder} is not bound to an item + * or the given {@link Adapter} is not part of this Adapter (if this Adapter merges other + * adapters). */ - public final boolean isItemPrefetchEnabled() { - return mItemPrefetchEnabled; + public int findRelativeAdapterPositionIn( + @NonNull Adapter adapter, + @NonNull ViewHolder viewHolder, + int localPosition + ) { + if (adapter == this) { + return localPosition; + } + return NO_POSITION; } /** - * Sets whether the LayoutManager should be queried for views outside of - * its viewport while the UI thread is idle between frames. - * - *

    If enabled, the LayoutManager will be queried for items to inflate/bind in between - * view system traversals on devices running API 21 or greater. Default value is true.

    - * - *

    On platforms API level 21 and higher, the UI thread is idle between passing a frame - * to RenderThread and the starting up its next frame at the next VSync pulse. By - * prefetching out of window views in this time period, delays from inflation and view - * binding are much less likely to cause jank and stuttering during scrolls and flings.

    - * - *

    While prefetch is enabled, it will have the side effect of expanding the effective - * size of the View cache to hold prefetched views.

    + * This method calls {@link #onCreateViewHolder(ViewGroup, int)} to create a new + * {@link ViewHolder} and initializes some private fields to be used by RecyclerView. * - * @param enabled True if items should be prefetched in between traversals. - * @see #isItemPrefetchEnabled() + * @see #onCreateViewHolder(ViewGroup, int) */ - public final void setItemPrefetchEnabled(boolean enabled) { - if (enabled != mItemPrefetchEnabled) { - mItemPrefetchEnabled = enabled; - mPrefetchMaxCountObserved = 0; - if (mRecyclerView != null) { - mRecyclerView.mRecycler.updateViewCacheSize(); + @NonNull + public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) { + try { + Trace.beginSection(TRACE_CREATE_VIEW_TAG); + final VH holder = onCreateViewHolder(parent, viewType); + if (holder.itemView.getParent() != null) { + throw new IllegalStateException("ViewHolder views must not be attached when" + + " created. Ensure that you are not passing 'true' to the attachToRoot" + + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)"); } + holder.mItemViewType = viewType; + return holder; + } finally { + Trace.endSection(); } } /** - * Gather all positions from the LayoutManager to be prefetched, given specified momentum. - * - *

    If item prefetch is enabled, this method is called in between traversals to gather - * which positions the LayoutManager will soon need, given upcoming movement in subsequent - * traversals.

    + * This method internally calls {@link #onBindViewHolder(ViewHolder, int)} to update the + * {@link ViewHolder} contents with the item at the given position and also sets up some + * private fields to be used by RecyclerView. * - *

    The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for - * each item to be prepared, and these positions will have their ViewHolders created and - * bound, if there is sufficient time available, in advance of being needed by a - * scroll or layout.

    + * Adapters that merge other adapters should use + * {@link #bindViewHolder(ViewHolder, int)} when calling nested adapters so that + * RecyclerView can track which adapter bound the {@link ViewHolder} to return the correct + * position from {@link ViewHolder#getBindingAdapterPosition()} method. + * They should also override + * the {@link #findRelativeAdapterPositionIn(Adapter, ViewHolder, int)} method. * - * @param dx X movement component. - * @param dy Y movement component. - * @param state State of RecyclerView - * @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into. - * @see #isItemPrefetchEnabled() - * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + * @param holder The view holder whose contents should be updated + * @param position The position of the holder with respect to this adapter + * @see #onBindViewHolder(ViewHolder, int) */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void collectAdjacentPrefetchPositions(int dx, int dy, State state, - LayoutPrefetchRegistry layoutPrefetchRegistry) { + public final void bindViewHolder(@NonNull VH holder, int position) { + boolean rootBind = holder.mBindingAdapter == null; + if (rootBind) { + holder.mPosition = position; + if (hasStableIds()) { + holder.mItemId = getItemId(position); + } + holder.setFlags(ViewHolder.FLAG_BOUND, + ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID + | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN); + Trace.beginSection(TRACE_BIND_VIEW_TAG); + } + holder.mBindingAdapter = this; + onBindViewHolder(holder, position, holder.getUnmodifiedPayloads()); + if (rootBind) { + holder.clearPayload(); + final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams(); + if (layoutParams instanceof RecyclerView.LayoutParams) { + ((LayoutParams) layoutParams).mInsetsDirty = true; + } + Trace.endSection(); + } } /** - * Gather all positions from the LayoutManager to be prefetched in preperation for its - * RecyclerView to come on screen, due to the movement of another, containing RecyclerView. - * - *

    This method is only called when a RecyclerView is nested in another RecyclerView.

    - * - *

    If item prefetch is enabled for this LayoutManager, as well in another containing - * LayoutManager, this method is called in between draw traversals to gather - * which positions this LayoutManager will first need, once it appears on the screen.

    - * - *

    For example, if this LayoutManager represents a horizontally scrolling list within a - * vertically scrolling LayoutManager, this method would be called when the horizontal list - * is about to come onscreen.

    + * Return the view type of the item at position for the purposes + * of view recycling. * - *

    The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for - * each item to be prepared, and these positions will have their ViewHolders created and - * bound, if there is sufficient time available, in advance of being needed by a - * scroll or layout.

    + *

    The default implementation of this method returns 0, making the assumption of + * a single view type for the adapter. Unlike ListView adapters, types need not + * be contiguous. Consider using id resources to uniquely identify item view types. * - * @param adapterItemCount number of items in the associated adapter. - * @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into. - * @see #isItemPrefetchEnabled() - * @see #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry) + * @param position position to query + * @return integer value identifying the type of the view needed to represent the item at + * position. Type codes need not be contiguous. */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void collectInitialPrefetchPositions(int adapterItemCount, - LayoutPrefetchRegistry layoutPrefetchRegistry) { - } - - void dispatchAttachedToWindow(RecyclerView view) { - mIsAttachedToWindow = true; - onAttachedToWindow(view); - } - - void dispatchDetachedFromWindow(RecyclerView view, Recycler recycler) { - mIsAttachedToWindow = false; - onDetachedFromWindow(view, recycler); + public int getItemViewType(int position) { + return 0; } /** - * Returns whether LayoutManager is currently attached to a RecyclerView which is attached - * to a window. + * Indicates whether each item in the data set can be represented with a unique identifier + * of type {@link java.lang.Long}. * - * @return True if this LayoutManager is controlling a RecyclerView and the RecyclerView - * is attached to window. + * @param hasStableIds Whether items in data set have unique identifiers or not. + * @see #hasStableIds() + * @see #getItemId(int) */ - public boolean isAttachedToWindow() { - return mIsAttachedToWindow; + public void setHasStableIds(boolean hasStableIds) { + if (hasObservers()) { + throw new IllegalStateException("Cannot change whether this adapter has " + + "stable IDs while the adapter has registered observers."); + } + mHasStableIds = hasStableIds; } /** - * Causes the Runnable to execute on the next animation time step. - * The runnable will be run on the user interface thread. - *

    - * Calling this method when LayoutManager is not attached to a RecyclerView has no effect. + * Return the stable ID for the item at position. If {@link #hasStableIds()} + * would return false this method should return {@link #NO_ID}. The default implementation + * of this method returns {@link #NO_ID}. * - * @param action The Runnable that will be executed. - * @see #removeCallbacks + * @param position Adapter position to query + * @return the stable ID of the item at position */ - public void postOnAnimation(Runnable action) { - if (mRecyclerView != null) { - ViewCompat.postOnAnimation(mRecyclerView, action); - } + public long getItemId(int position) { + return NO_ID; } /** - * Removes the specified Runnable from the message queue. - *

    - * Calling this method when LayoutManager is not attached to a RecyclerView has no effect. + * Returns the total number of items in the data set held by the adapter. * - * @param action The Runnable to remove from the message handling queue - * @return true if RecyclerView could ask the Handler to remove the Runnable, - * false otherwise. When the returned value is true, the Runnable - * may or may not have been actually removed from the message queue - * (for instance, if the Runnable was not in the queue already.) - * @see #postOnAnimation + * @return The total number of items in this adapter. */ - public boolean removeCallbacks(Runnable action) { - if (mRecyclerView != null) { - return mRecyclerView.removeCallbacks(action); - } - return false; - } + public abstract int getItemCount(); /** - * Called when this LayoutManager is both attached to a RecyclerView and that RecyclerView - * is attached to a window. - *

    - * If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not - * call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was - * not requested on the RecyclerView while it was detached. - *

    - * Subclass implementations should always call through to the superclass implementation. + * Returns true if this adapter publishes a unique long value that can + * act as a key for the item at a given position in the data set. If that item is relocated + * in the data set, the ID returned for that item should be the same. * - * @param view The RecyclerView this LayoutManager is bound to - * @see #onDetachedFromWindow(RecyclerView, Recycler) + * @return true if this adapter's items have stable IDs */ - @CallSuper - public void onAttachedToWindow(RecyclerView view) { + public final boolean hasStableIds() { + return mHasStableIds; } /** - * @deprecated override {@link #onDetachedFromWindow(RecyclerView, Recycler)} + * Called when a view created by this adapter has been recycled. + * + *

    A view is recycled when a {@link LayoutManager} decides that it no longer + * needs to be attached to its parent {@link RecyclerView}. This can be because it has + * fallen out of visibility or a set of cached views represented by views still + * attached to the parent RecyclerView. If an item view has large or expensive data + * bound to it such as large bitmaps, this may be a good place to release those + * resources.

    + *

    + * RecyclerView calls this method right before clearing ViewHolder's internal data and + * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information + * before being recycled, you can call {@link ViewHolder#getBindingAdapterPosition()} to get + * its adapter position. + * + * @param holder The ViewHolder for the view being recycled */ - @Deprecated - public void onDetachedFromWindow(RecyclerView view) { - + public void onViewRecycled(@NonNull VH holder) { } /** - * Called when this LayoutManager is detached from its parent RecyclerView or when - * its parent RecyclerView is detached from its window. - *

    - * LayoutManager should clear all of its View references as another LayoutManager might be - * assigned to the RecyclerView. + * Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled + * due to its transient state. Upon receiving this callback, Adapter can clear the + * animation(s) that effect the View's transient state and return true so that + * the View can be recycled. Keep in mind that the View in question is already removed from + * the RecyclerView. *

    - * If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not - * call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was - * not requested on the RecyclerView while it was detached. + * In some cases, it is acceptable to recycle a View although it has transient state. Most + * of the time, this is a case where the transient state will be cleared in + * {@link #onBindViewHolder(ViewHolder, int)} call when View is rebound to a new position. + * For this reason, RecyclerView leaves the decision to the Adapter and uses the return + * value of this method to decide whether the View should be recycled or not. *

    - * If your LayoutManager has View references that it cleans in on-detach, it should also - * call {@link RecyclerView#requestLayout()} to ensure that it is re-laid out when - * RecyclerView is re-attached. + * Note that when all animations are created by {@link RecyclerView.ItemAnimator}, you + * should never receive this callback because RecyclerView keeps those Views as children + * until their animations are complete. This callback is useful when children of the item + * views create animations which may not be easy to implement using an {@link ItemAnimator}. *

    - * Subclass implementations should always call through to the superclass implementation. + * You should never fix this issue by calling + * holder.itemView.setHasTransientState(false); unless you've previously called + * holder.itemView.setHasTransientState(true);. Each + * View.setHasTransientState(true) call must be matched by a + * View.setHasTransientState(false) call, otherwise, the state of the View + * may become inconsistent. You should always prefer to end or cancel animations that are + * triggering the transient state instead of handling it manually. * - * @param view The RecyclerView this LayoutManager is bound to - * @param recycler The recycler to use if you prefer to recycle your children instead of - * keeping them around. - * @see #onAttachedToWindow(RecyclerView) + * @param holder The ViewHolder containing the View that could not be recycled due to its + * transient state. + * @return True if the View should be recycled, false otherwise. Note that if this method + * returns true, RecyclerView will ignore the transient state of + * the View and recycle it regardless. If this method returns false, + * RecyclerView will check the View's transient state again before giving a final decision. + * Default implementation returns false. */ - @CallSuper - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void onDetachedFromWindow(RecyclerView view, Recycler recycler) { - onDetachedFromWindow(view); + public boolean onFailedToRecycleView(@NonNull VH holder) { + return false; } /** - * Check if the RecyclerView is configured to clip child views to its padding. + * Called when a view created by this adapter has been attached to a window. * - * @return true if this RecyclerView clips children to its padding, false otherwise + *

    This can be used as a reasonable signal that the view is about to be seen + * by the user. If the adapter previously freed any resources in + * {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder) onViewDetachedFromWindow} + * those resources should be restored here.

    + * + * @param holder Holder of the view being attached */ - public boolean getClipToPadding() { - return mRecyclerView != null && mRecyclerView.mClipToPadding; + public void onViewAttachedToWindow(@NonNull VH holder) { } /** - * Lay out all relevant child views from the given adapter. - *

    - * The LayoutManager is in charge of the behavior of item animations. By default, - * RecyclerView has a non-null {@link #getItemAnimator() ItemAnimator}, and simple - * item animations are enabled. This means that add/remove operations on the - * adapter will result in animations to add new or appearing items, removed or - * disappearing items, and moved items. If a LayoutManager returns false from - * {@link #supportsPredictiveItemAnimations()}, which is the default, and runs a - * normal layout operation during {@link #onLayoutChildren(Recycler, State)}, the - * RecyclerView will have enough information to run those animations in a simple - * way. For example, the default ItemAnimator, {@link DefaultItemAnimator}, will - * simply fade views in and out, whether they are actually added/removed or whether - * they are moved on or off the screen due to other add/remove operations. - * - *

    A LayoutManager wanting a better item animation experience, where items can be - * animated onto and off of the screen according to where the items exist when they - * are not on screen, then the LayoutManager should return true from - * {@link #supportsPredictiveItemAnimations()} and add additional logic to - * {@link #onLayoutChildren(Recycler, State)}. Supporting predictive animations - * means that {@link #onLayoutChildren(Recycler, State)} will be called twice; - * once as a "pre" layout step to determine where items would have been prior to - * a real layout, and again to do the "real" layout. In the pre-layout phase, - * items will remember their pre-layout positions to allow them to be laid out - * appropriately. Also, {@link LayoutParams#isItemRemoved() removed} items will - * be returned from the scrap to help determine correct placement of other items. - * These removed items should not be added to the child list, but should be used - * to help calculate correct positioning of other views, including views that - * were not previously onscreen (referred to as APPEARING views), but whose - * pre-layout offscreen position can be determined given the extra - * information about the pre-layout removed views.

    - * - *

    The second layout pass is the real layout in which only non-removed views - * will be used. The only additional requirement during this pass is, if - * {@link #supportsPredictiveItemAnimations()} returns true, to note which - * views exist in the child list prior to layout and which are not there after - * layout (referred to as DISAPPEARING views), and to position/layout those views - * appropriately, without regard to the actual bounds of the RecyclerView. This allows - * the animation system to know the location to which to animate these disappearing - * views.

    + * Called when a view created by this adapter has been detached from its window. * - *

    The default LayoutManager implementations for RecyclerView handle all of these - * requirements for animations already. Clients of RecyclerView can either use one - * of these layout managers directly or look at their implementations of - * onLayoutChildren() to see how they account for the APPEARING and - * DISAPPEARING views.

    + *

    Becoming detached from the window is not necessarily a permanent condition; + * the consumer of an Adapter's views may choose to cache views offscreen while they + * are not visible, attaching and detaching them as appropriate.

    * - * @param recycler Recycler to use for fetching potentially cached views for a - * position - * @param state Transient state of RecyclerView + * @param holder Holder of the view being detached */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void onLayoutChildren(Recycler recycler, State state) { - Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) "); + public void onViewDetachedFromWindow(@NonNull VH holder) { } /** - * Called after a full layout calculation is finished. The layout calculation may include - * multiple {@link #onLayoutChildren(Recycler, State)} calls due to animations or - * layout measurement but it will include only one {@link #onLayoutCompleted(State)} call. - * This method will be called at the end of {@link View#layout(int, int, int, int)} call. - *

    - * This is a good place for the LayoutManager to do some cleanup like pending scroll - * position, saved state etc. + * Returns true if one or more observers are attached to this adapter. * - * @param state Transient state of RecyclerView + * @return true if this adapter has observers */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void onLayoutCompleted(State state) { + public final boolean hasObservers() { + return mObservable.hasObservers(); } /** - * Create a default LayoutParams object for a child of the RecyclerView. - * - *

    LayoutManagers will often want to use a custom LayoutParams type - * to store extra information specific to the layout. Client code should subclass - * {@link RecyclerView.LayoutParams} for this purpose.

    - * - *

    Important: if you use your own custom LayoutParams type - * you must also override - * {@link #checkLayoutParams(LayoutParams)}, - * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and - * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.

    + * Register a new observer to listen for data changes. * - * @return A new LayoutParams for a child view - */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public abstract LayoutParams generateDefaultLayoutParams(); - - /** - * Determines the validity of the supplied LayoutParams object. + *

    The adapter may publish a variety of events describing specific changes. + * Not all adapters may support all change types and some may fall back to a generic + * {@link RecyclerView.AdapterDataObserver#onChanged() + * "something changed"} event if more specific data is not available.

    * - *

    This should check to make sure that the object is of the correct type - * and all values are within acceptable ranges. The default implementation - * returns true for non-null params.

    + *

    Components registering observers with an adapter are responsible for + * {@link #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver) + * unregistering} those observers when finished.

    * - * @param lp LayoutParams object to check - * @return true if this LayoutParams object is valid, false otherwise + * @param observer Observer to register + * @see #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver) */ - public boolean checkLayoutParams(LayoutParams lp) { - return lp != null; + public void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) { + mObservable.registerObserver(observer); } /** - * Create a LayoutParams object suitable for this LayoutManager, copying relevant - * values from the supplied LayoutParams object if possible. + * Unregister an observer currently listening for data changes. * - *

    Important: if you use your own custom LayoutParams type - * you must also override - * {@link #checkLayoutParams(LayoutParams)}, - * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and - * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.

    + *

    The unregistered observer will no longer receive events about changes + * to the adapter.

    * - * @param lp Source LayoutParams object to copy values from - * @return a new LayoutParams object + * @param observer Observer to unregister + * @see #registerAdapterDataObserver(RecyclerView.AdapterDataObserver) */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { - if (lp instanceof LayoutParams) { - return new LayoutParams((LayoutParams) lp); - } else if (lp instanceof MarginLayoutParams) { - return new LayoutParams((MarginLayoutParams) lp); - } else { - return new LayoutParams(lp); - } + public void unregisterAdapterDataObserver(@NonNull AdapterDataObserver observer) { + mObservable.unregisterObserver(observer); } /** - * Create a LayoutParams object suitable for this LayoutManager from - * an inflated layout resource. - * - *

    Important: if you use your own custom LayoutParams type - * you must also override - * {@link #checkLayoutParams(LayoutParams)}, - * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and - * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.

    + * Called by RecyclerView when it starts observing this Adapter. + *

    + * Keep in mind that same adapter may be observed by multiple RecyclerViews. * - * @param c Context for obtaining styled attributes - * @param attrs AttributeSet describing the supplied arguments - * @return a new LayoutParams object + * @param recyclerView The RecyclerView instance which started observing this adapter. + * @see #onDetachedFromRecyclerView(RecyclerView) */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { - return new LayoutParams(c, attrs); + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { } /** - * Scroll horizontally by dx pixels in screen coordinates and return the distance traveled. - * The default implementation does nothing and returns 0. + * Called by RecyclerView when it stops observing this Adapter. * - * @param dx distance to scroll by in pixels. X increases as scroll position - * approaches the right. - * @param recycler Recycler to use for fetching potentially cached views for a - * position - * @param state Transient state of RecyclerView - * @return The actual distance scrolled. The return value will be negative if dx was - * negative and scrolling proceeeded in that direction. - * Math.abs(result) may be less than dx if a boundary was reached. + * @param recyclerView The RecyclerView instance which stopped observing this adapter. + * @see #onAttachedToRecyclerView(RecyclerView) */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public int scrollHorizontallyBy(int dx, Recycler recycler, State state) { - return 0; + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { } /** - * Scroll vertically by dy pixels in screen coordinates and return the distance traveled. - * The default implementation does nothing and returns 0. + * Notify any registered observers that the data set has changed. * - * @param dy distance to scroll in pixels. Y increases as scroll position - * approaches the bottom. - * @param recycler Recycler to use for fetching potentially cached views for a - * position - * @param state Transient state of RecyclerView - * @return The actual distance scrolled. The return value will be negative if dy was - * negative and scrolling proceeeded in that direction. - * Math.abs(result) may be less than dy if a boundary was reached. + *

    There are two different classes of data change events, item changes and structural + * changes. Item changes are when a single item has its data updated but no positional + * changes have occurred. Structural changes are when items are inserted, removed or moved + * within the data set.

    + * + *

    This event does not specify what about the data set has changed, forcing + * any observers to assume that all existing items and structure may no longer be valid. + * LayoutManagers will be forced to fully rebind and relayout all visible views.

    + * + *

    RecyclerView will attempt to synthesize visible structural change events + * for adapters that report that they have {@link #hasStableIds() stable IDs} when + * this method is used. This can help for the purposes of animation and visual + * object persistence but individual item views will still need to be rebound + * and relaid out.

    + * + *

    If you are writing an adapter it will always be more efficient to use the more + * specific change events if you can. Rely on notifyDataSetChanged() + * as a last resort.

    + * + * @see #notifyItemChanged(int) + * @see #notifyItemInserted(int) + * @see #notifyItemRemoved(int) + * @see #notifyItemRangeChanged(int, int) + * @see #notifyItemRangeInserted(int, int) + * @see #notifyItemRangeRemoved(int, int) */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public int scrollVerticallyBy(int dy, Recycler recycler, State state) { - return 0; + public final void notifyDataSetChanged() { + mObservable.notifyChanged(); } /** - * Query if horizontal scrolling is currently supported. The default implementation - * returns false. + * Notify any registered observers that the item at position has changed. + * Equivalent to calling notifyItemChanged(position, null);. * - * @return True if this LayoutManager can scroll the current contents horizontally + *

    This is an item change event, not a structural change event. It indicates that any + * reflection of the data at position is out of date and should be updated. + * The item at position retains the same identity.

    + * + * @param position Position of the item that has changed + * @see #notifyItemRangeChanged(int, int) */ - public boolean canScrollHorizontally() { - return false; + public final void notifyItemChanged(int position) { + mObservable.notifyItemRangeChanged(position, 1); } /** - * Query if vertical scrolling is currently supported. The default implementation - * returns false. + * Notify any registered observers that the item at position has changed with + * an optional payload object. * - * @return True if this LayoutManager can scroll the current contents vertically + *

    This is an item change event, not a structural change event. It indicates that any + * reflection of the data at position is out of date and should be updated. + * The item at position retains the same identity. + *

    + * + *

    + * Client can optionally pass a payload for partial change. These payloads will be merged + * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the + * item is already represented by a ViewHolder and it will be rebound to the same + * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing + * payloads on that item and prevent future payload until + * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume + * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not + * attached, the payload will be simply dropped. + * + * @param position Position of the item that has changed + * @param payload Optional parameter, use null to identify a "full" update + * @see #notifyItemRangeChanged(int, int) */ - public boolean canScrollVertically() { - return false; + public final void notifyItemChanged(int position, @Nullable Object payload) { + mObservable.notifyItemRangeChanged(position, 1, payload); } /** - * Scroll to the specified adapter position. - *

    - * Actual position of the item on the screen depends on the LayoutManager implementation. + * Notify any registered observers that the itemCount items starting at + * position positionStart have changed. + * Equivalent to calling notifyItemRangeChanged(position, itemCount, null);. * - * @param position Scroll to this adapter position. + *

    This is an item change event, not a structural change event. It indicates that + * any reflection of the data in the given position range is out of date and should + * be updated. The items in the given range retain the same identity.

    + * + * @param positionStart Position of the first item that has changed + * @param itemCount Number of items that have changed + * @see #notifyItemChanged(int) */ - public void scrollToPosition(int position) { - if (DEBUG) { - Log.e(TAG, "You MUST implement scrollToPosition. It will soon become abstract"); - } + public final void notifyItemRangeChanged(int positionStart, int itemCount) { + mObservable.notifyItemRangeChanged(positionStart, itemCount); } /** - *

    Smooth scroll to the specified adapter position.

    - *

    To support smooth scrolling, override this method, create your {@link SmoothScroller} - * instance and call {@link #startSmoothScroll(SmoothScroller)}. + * Notify any registered observers that the itemCount items starting at + * position positionStart have changed. An optional payload can be + * passed to each changed item. + * + *

    This is an item change event, not a structural change event. It indicates that any + * reflection of the data in the given position range is out of date and should be updated. + * The items in the given range retain the same identity. *

    * - * @param recyclerView The RecyclerView to which this layout manager is attached - * @param state Current State of RecyclerView - * @param position Scroll to this adapter position. + *

    + * Client can optionally pass a payload for partial change. These payloads will be merged + * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the + * item is already represented by a ViewHolder and it will be rebound to the same + * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing + * payloads on that item and prevent future payload until + * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume + * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not + * attached, the payload will be simply dropped. + * + * @param positionStart Position of the first item that has changed + * @param itemCount Number of items that have changed + * @param payload Optional parameter, use null to identify a "full" update + * @see #notifyItemChanged(int) */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void smoothScrollToPosition(RecyclerView recyclerView, State state, - int position) { - Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling"); + public final void notifyItemRangeChanged(int positionStart, int itemCount, + @Nullable Object payload) { + mObservable.notifyItemRangeChanged(positionStart, itemCount, payload); } /** - * Starts a smooth scroll using the provided {@link SmoothScroller}. - * - *

    Each instance of SmoothScroller is intended to only be used once. Provide a new - * SmoothScroller instance each time this method is called. + * Notify any registered observers that the item reflected at position + * has been newly inserted. The item previously at position is now at + * position position + 1. * - *

    Calling this method will cancel any previous smooth scroll request. + *

    This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered.

    * - * @param smoothScroller Instance which defines how smooth scroll should be animated + * @param position Position of the newly inserted item in the data set + * @see #notifyItemRangeInserted(int, int) */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void startSmoothScroll(SmoothScroller smoothScroller) { - if (mSmoothScroller != null && smoothScroller != mSmoothScroller - && mSmoothScroller.isRunning()) { - mSmoothScroller.stop(); - } - mSmoothScroller = smoothScroller; - mSmoothScroller.start(mRecyclerView, this); + public final void notifyItemInserted(int position) { + mObservable.notifyItemRangeInserted(position, 1); } /** - * @return true if RecyclerView is currently in the state of smooth scrolling. + * Notify any registered observers that the item reflected at fromPosition + * has been moved to toPosition. + * + *

    This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered.

    + * + * @param fromPosition Previous position of the item. + * @param toPosition New position of the item. */ - public boolean isSmoothScrolling() { - return mSmoothScroller != null && mSmoothScroller.isRunning(); + public final void notifyItemMoved(int fromPosition, int toPosition) { + mObservable.notifyItemMoved(fromPosition, toPosition); } /** - * Returns the resolved layout direction for this RecyclerView. + * Notify any registered observers that the currently reflected itemCount + * items starting at positionStart have been newly inserted. The items + * previously located at positionStart and beyond can now be found starting + * at position positionStart + itemCount. * - * @return {@link androidx.core.view.ViewCompat#LAYOUT_DIRECTION_RTL} if the layout - * direction is RTL or returns - * {@link androidx.core.view.ViewCompat#LAYOUT_DIRECTION_LTR} if the layout direction - * is not RTL. + *

    This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.

    + * + * @param positionStart Position of the first item that was inserted + * @param itemCount Number of items inserted + * @see #notifyItemInserted(int) */ - public int getLayoutDirection() { - return ViewCompat.getLayoutDirection(mRecyclerView); + public final void notifyItemRangeInserted(int positionStart, int itemCount) { + mObservable.notifyItemRangeInserted(positionStart, itemCount); } /** - * Ends all animations on the view created by the {@link ItemAnimator}. + * Notify any registered observers that the item previously located at position + * has been removed from the data set. The items previously located at and after + * position may now be found at oldPosition - 1. * - * @param view The View for which the animations should be ended. - * @see RecyclerView.ItemAnimator#endAnimations() + *

    This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.

    + * + * @param position Position of the item that has now been removed + * @see #notifyItemRangeRemoved(int, int) */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void endAnimation(View view) { - if (mRecyclerView.mItemAnimator != null) { - mRecyclerView.mItemAnimator.endAnimation(getChildViewHolderInt(view)); - } + public final void notifyItemRemoved(int position) { + mObservable.notifyItemRangeRemoved(position, 1); } /** - * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view - * to the layout that is known to be going away, either because it has been - * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the - * visible portion of the container but is being laid out in order to inform RecyclerView - * in how to animate the item out of view. - *

    - * Views added via this method are going to be invisible to LayoutManager after the - * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)} - * or won't be included in {@link #getChildCount()} method. + * Notify any registered observers that the itemCount items previously + * located at positionStart have been removed from the data set. The items + * previously located at and after positionStart + itemCount may now be found + * at oldPosition - itemCount. * - * @param child View to add and then remove with animation. + *

    This is a structural change event. Representations of other existing items in the data + * set are still considered up to date and will not be rebound, though their positions + * may be altered.

    + * + * @param positionStart Previous position of the first item that was removed + * @param itemCount Number of items removed from the data set */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void addDisappearingView(View child) { - addDisappearingView(child, -1); + public final void notifyItemRangeRemoved(int positionStart, int itemCount) { + mObservable.notifyItemRangeRemoved(positionStart, itemCount); } /** - * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view - * to the layout that is known to be going away, either because it has been - * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the - * visible portion of the container but is being laid out in order to inform RecyclerView - * in how to animate the item out of view. + * Sets the state restoration strategy for the Adapter. + * + * By default, it is set to {@link StateRestorationPolicy#ALLOW} which means RecyclerView + * expects any set Adapter to be immediately capable of restoring the RecyclerView's saved + * scroll position. + *

    + * This behaviour might be undesired if the Adapter's data is loaded asynchronously, and + * thus unavailable during initial layout (e.g. after Activity rotation). To avoid losing + * scroll position, you can change this to be either + * {@link StateRestorationPolicy#PREVENT_WHEN_EMPTY} or + * {@link StateRestorationPolicy#PREVENT}. + * Note that the former means your RecyclerView will restore state as soon as Adapter has + * 1 or more items while the latter requires you to call + * {@link #setStateRestorationPolicy(StateRestorationPolicy)} with either + * {@link StateRestorationPolicy#ALLOW} or + * {@link StateRestorationPolicy#PREVENT_WHEN_EMPTY} again when the Adapter is + * ready to restore its state. *

    - * Views added via this method are going to be invisible to LayoutManager after the - * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)} - * or won't be included in {@link #getChildCount()} method. + * RecyclerView will still layout even when State restoration is disabled. The behavior of + * how State is restored is up to the {@link LayoutManager}. All default LayoutManagers + * will override current state with restored state when state restoration happens (unless + * an explicit call to {@link LayoutManager#scrollToPosition(int)} is made). + *

    + * Calling this method after state is restored will not have any effect other than changing + * the return value of {@link #getStateRestorationPolicy()}. * - * @param child View to add and then remove with animation. - * @param index Index of the view. + * @param strategy The saved state restoration strategy for this Adapter. + * @see #getStateRestorationPolicy() */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void addDisappearingView(View child, int index) { - addViewInt(child, index, true); + public void setStateRestorationPolicy(@NonNull StateRestorationPolicy strategy) { + mStateRestorationPolicy = strategy; + mObservable.notifyStateRestorationPolicyChanged(); } /** - * Add a view to the currently attached RecyclerView if needed. LayoutManagers should - * use this method to add views obtained from a {@link Recycler} using - * {@link Recycler#getViewForPosition(int)}. + * Returns when this Adapter wants to restore the state. * - * @param child View to add + * @return The current {@link StateRestorationPolicy} for this Adapter. Defaults to + * {@link StateRestorationPolicy#ALLOW}. + * @see #setStateRestorationPolicy(StateRestorationPolicy) */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void addView(View child) { - addView(child, -1); + @NonNull + public final StateRestorationPolicy getStateRestorationPolicy() { + return mStateRestorationPolicy; } /** - * Add a view to the currently attached RecyclerView if needed. LayoutManagers should - * use this method to add views obtained from a {@link Recycler} using - * {@link Recycler#getViewForPosition(int)}. + * Called by the RecyclerView to decide whether the SavedState should be given to the + * LayoutManager or not. * - * @param child View to add - * @param index Index to add child at + * @return {@code true} if the Adapter is ready to restore its state, {@code false} + * otherwise. */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void addView(View child, int index) { - addViewInt(child, index, false); - } - - private void addViewInt(View child, int index, boolean disappearing) { - ViewHolder holder = getChildViewHolderInt(child); - if (disappearing || holder.isRemoved()) { - // these views will be hidden at the end of the layout pass. - mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder); - } else { - // This may look like unnecessary but may happen if layout manager supports - // predictive layouts and adapter removed then re-added the same item. - // In this case, added version will be visible in the post layout (because add is - // deferred) but RV will still bind it to the same View. - // So if a View re-appears in post layout pass, remove it from disappearing list. - mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder); - } - LayoutParams lp = (LayoutParams) child.getLayoutParams(); - if (holder.wasReturnedFromScrap() || holder.isScrap()) { - if (holder.isScrap()) { - holder.unScrap(); - } else { - holder.clearReturnedFromScrapFlag(); - } - mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false); - if (DISPATCH_TEMP_DETACH) { - ViewCompat.dispatchFinishTemporaryDetach(child); - } - } else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child - // ensure in correct position - int currentIndex = mChildHelper.indexOfChild(child); - if (index == -1) { - index = mChildHelper.getChildCount(); - } - if (currentIndex == -1) { - throw new IllegalStateException("Added View has RecyclerView as parent but" - + " view is not a real child. Unfiltered index:" - + mRecyclerView.indexOfChild(child) + mRecyclerView.exceptionLabel()); - } - if (currentIndex != index) { - mRecyclerView.mLayout.moveView(currentIndex, index); - } - } else { - mChildHelper.addView(child, index, false); - lp.mInsetsDirty = true; - if (mSmoothScroller != null && mSmoothScroller.isRunning()) { - mSmoothScroller.onChildAttachedToWindow(child); - } - } - if (lp.mPendingInvalidate) { - if (DEBUG) { - Log.d(TAG, "consuming pending invalidate on child " + lp.mViewHolder); - } - holder.itemView.invalidate(); - lp.mPendingInvalidate = false; + boolean canRestoreState() { + switch (mStateRestorationPolicy) { + case PREVENT: + return false; + case PREVENT_WHEN_EMPTY: + return getItemCount() > 0; + default: + return true; } } /** - * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should - * use this method to completely remove a child view that is no longer needed. - * LayoutManagers should strongly consider recycling removed views using - * {@link Recycler#recycleView(android.view.View)}. - * - * @param child View to remove + * Defines how this Adapter wants to restore its state after a view reconstruction (e.g. + * configuration change). */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void removeView(View child) { - mChildHelper.removeView(child); + public enum StateRestorationPolicy { + /** + * Adapter is ready to restore State immediately, RecyclerView will provide the state + * to the LayoutManager in the next layout pass. + */ + ALLOW, + /** + * Adapter is ready to restore State when it has more than 0 items. RecyclerView will + * provide the state to the LayoutManager as soon as the Adapter has 1 or more items. + */ + PREVENT_WHEN_EMPTY, + /** + * RecyclerView will not restore the state for the Adapter until a call to + * {@link #setStateRestorationPolicy(StateRestorationPolicy)} is made with either + * {@link #ALLOW} or {@link #PREVENT_WHEN_EMPTY}. + */ + PREVENT } + } - /** - * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should - * use this method to completely remove a child view that is no longer needed. - * LayoutManagers should strongly consider recycling removed views using - * {@link Recycler#recycleView(android.view.View)}. - * - * @param index Index of the child view to remove - */ - public void removeViewAt(int index) { - View child = getChildAt(index); - if (child != null) { - mChildHelper.removeViewAt(index); + @SuppressWarnings("unchecked") + void dispatchChildDetached(View child) { + final ViewHolder viewHolder = getChildViewHolderInt(child); + onChildDetachedFromWindow(child); + if (mAdapter != null && viewHolder != null) { + mAdapter.onViewDetachedFromWindow(viewHolder); + } + if (mOnChildAttachStateListeners != null) { + final int cnt = mOnChildAttachStateListeners.size(); + for (int i = cnt - 1; i >= 0; i--) { + mOnChildAttachStateListeners.get(i).onChildViewDetachedFromWindow(child); } } + } - /** - * Remove all views from the currently attached RecyclerView. This will not recycle - * any of the affected views; the LayoutManager is responsible for doing so if desired. - */ - public void removeAllViews() { - // Only remove non-animating views - int childCount = getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - mChildHelper.removeViewAt(i); + @SuppressWarnings("unchecked") + void dispatchChildAttached(View child) { + final ViewHolder viewHolder = getChildViewHolderInt(child); + onChildAttachedToWindow(child); + if (mAdapter != null && viewHolder != null) { + mAdapter.onViewAttachedToWindow(viewHolder); + } + if (mOnChildAttachStateListeners != null) { + final int cnt = mOnChildAttachStateListeners.size(); + for (int i = cnt - 1; i >= 0; i--) { + mOnChildAttachStateListeners.get(i).onChildViewAttachedToWindow(child); } } + } + + /** + * A LayoutManager is responsible for measuring and positioning item views + * within a RecyclerView as well as determining the policy for when to recycle + * item views that are no longer visible to the user. By changing the LayoutManager + * a RecyclerView can be used to implement a standard vertically scrolling list, + * a uniform grid, staggered grids, horizontally scrolling collections and more. Several stock + * layout managers are provided for general use. + *

    + * If the LayoutManager specifies a default constructor or one with the signature + * ({@link Context}, {@link AttributeSet}, {@code int}, {@code int}), RecyclerView will + * instantiate and set the LayoutManager when being inflated. Most used properties can + * be then obtained from {@link #getProperties(Context, AttributeSet, int, int)}. In case + * a LayoutManager specifies both constructors, the non-default constructor will take + * precedence. + */ + public abstract static class LayoutManager { + ChildHelper mChildHelper; + RecyclerView mRecyclerView; /** - * Returns offset of the RecyclerView's text baseline from the its top boundary. - * - * @return The offset of the RecyclerView's text baseline from the its top boundary; -1 if - * there is no baseline. + * The callback used for retrieving information about a RecyclerView and its children in the + * horizontal direction. */ - public int getBaseline() { - return -1; - } + private final ViewBoundsCheck.Callback mHorizontalBoundCheckCallback = + new ViewBoundsCheck.Callback() { + @Override + public View getChildAt(int index) { + return LayoutManager.this.getChildAt(index); + } + + @Override + public int getParentStart() { + return LayoutManager.this.getPaddingLeft(); + } + + @Override + public int getParentEnd() { + return LayoutManager.this.getWidth() - LayoutManager.this.getPaddingRight(); + } + + @Override + public int getChildStart(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return LayoutManager.this.getDecoratedLeft(view) - params.leftMargin; + } + + @Override + public int getChildEnd(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return LayoutManager.this.getDecoratedRight(view) + params.rightMargin; + } + }; /** - * Returns the adapter position of the item represented by the given View. This does not - * contain any adapter changes that might have happened after the last layout. - * - * @param view The view to query - * @return The adapter position of the item which is rendered by this View. + * The callback used for retrieving information about a RecyclerView and its children in the + * vertical direction. */ - public int getPosition(@NonNull View view) { - return ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition(); - } + private final ViewBoundsCheck.Callback mVerticalBoundCheckCallback = + new ViewBoundsCheck.Callback() { + @Override + public View getChildAt(int index) { + return LayoutManager.this.getChildAt(index); + } + + @Override + public int getParentStart() { + return LayoutManager.this.getPaddingTop(); + } + + @Override + public int getParentEnd() { + return LayoutManager.this.getHeight() + - LayoutManager.this.getPaddingBottom(); + } - /** - * Returns the View type defined by the adapter. - * - * @param view The view to query - * @return The type of the view assigned by the adapter. - */ - public int getItemViewType(@NonNull View view) { - return getChildViewHolderInt(view).getItemViewType(); - } + @Override + public int getChildStart(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return LayoutManager.this.getDecoratedTop(view) - params.topMargin; + } - /** - * Traverses the ancestors of the given view and returns the item view that contains it - * and also a direct child of the LayoutManager. - *

    - * Note that this method may return null if the view is a child of the RecyclerView but - * not a child of the LayoutManager (e.g. running a disappear animation). - * - * @param view The view that is a descendant of the LayoutManager. - * @return The direct child of the LayoutManager which contains the given view or null if - * the provided view is not a descendant of this LayoutManager. - * @see RecyclerView#getChildViewHolder(View) - * @see RecyclerView#findContainingViewHolder(View) - */ - @Nullable - public View findContainingItemView(@NonNull View view) { - if (mRecyclerView == null) { - return null; - } - View found = mRecyclerView.findContainingItemView(view); - if (found == null) { - return null; - } - if (mChildHelper.isHidden(found)) { - return null; - } - return found; - } + @Override + public int getChildEnd(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return LayoutManager.this.getDecoratedBottom(view) + params.bottomMargin; + } + }; /** - * Finds the view which represents the given adapter position. - *

    - * This method traverses each child since it has no information about child order. - * Override this method to improve performance if your LayoutManager keeps data about - * child views. - *

    - * If a view is ignored via {@link #ignoreView(View)}, it is also ignored by this method. + * Utility objects used to check the boundaries of children against their parent + * RecyclerView. * - * @param position Position of the item in adapter - * @return The child view that represents the given position or null if the position is not - * laid out + * @see #isViewPartiallyVisible(View, boolean, boolean), + * {@link LinearLayoutManager#findOneVisibleChild(int, int, boolean, boolean)}, + * and {@link LinearLayoutManager#findOnePartiallyOrCompletelyInvisibleChild(int, int)}. */ + ViewBoundsCheck mHorizontalBoundCheck = new ViewBoundsCheck(mHorizontalBoundCheckCallback); + ViewBoundsCheck mVerticalBoundCheck = new ViewBoundsCheck(mVerticalBoundCheckCallback); + @Nullable - public View findViewByPosition(int position) { - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - ViewHolder vh = getChildViewHolderInt(child); - if (vh == null) { - continue; - } - if (vh.getLayoutPosition() == position && !vh.shouldIgnore() - && (mRecyclerView.mState.isPreLayout() || !vh.isRemoved())) { - return child; - } - } - return null; - } + SmoothScroller mSmoothScroller; + + boolean mRequestedSimpleAnimations = false; + + boolean mIsAttachedToWindow = false; /** - * Temporarily detach a child view. - * - *

    LayoutManagers may want to perform a lightweight detach operation to rearrange - * views currently attached to the RecyclerView. Generally LayoutManager implementations - * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} - * so that the detached view may be rebound and reused.

    - * - *

    If a LayoutManager uses this method to detach a view, it must - * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach} - * or {@link #removeDetachedView(android.view.View) fully remove} the detached view - * before the LayoutManager entry point method called by RecyclerView returns.

    - * - * @param child Child to detach + * This field is only set via the deprecated {@link #setAutoMeasureEnabled(boolean)} and is + * only accessed via {@link #isAutoMeasureEnabled()} for backwards compatability reasons. */ - public void detachView(@NonNull View child) { - int ind = mChildHelper.indexOfChild(child); - if (ind >= 0) { - detachViewInternal(ind, child); - } - } + boolean mAutoMeasure = false; /** - * Temporarily detach a child view. - * - *

    LayoutManagers may want to perform a lightweight detach operation to rearrange - * views currently attached to the RecyclerView. Generally LayoutManager implementations - * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} - * so that the detached view may be rebound and reused.

    - * - *

    If a LayoutManager uses this method to detach a view, it must - * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach} - * or {@link #removeDetachedView(android.view.View) fully remove} the detached view - * before the LayoutManager entry point method called by RecyclerView returns.

    - * - * @param index Index of the child to detach + * LayoutManager has its own more strict measurement cache to avoid re-measuring a child + * if the space that will be given to it is already larger than what it has measured before. */ - public void detachViewAt(int index) { - detachViewInternal(index, getChildAt(index)); - } + private boolean mMeasurementCacheEnabled = true; - private void detachViewInternal(int index, @NonNull View view) { - if (DISPATCH_TEMP_DETACH) { - ViewCompat.dispatchStartTemporaryDetach(view); - } - mChildHelper.detachViewFromParent(index); - } + private boolean mItemPrefetchEnabled = true; /** - * Reattach a previously {@link #detachView(android.view.View) detached} view. - * This method should not be used to reattach views that were previously - * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. + * Written by {@link GapWorker} when prefetches occur to track largest number of view ever + * requested by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)} or + * {@link #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)} call. * - * @param child Child to reattach - * @param index Intended child index for child - * @param lp LayoutParams for child + * If expanded by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, + * will be reset upon layout to prevent initial prefetches (often large, since they're + * proportional to expected child count) from expanding cache permanently. */ - public void attachView(@NonNull View child, int index, LayoutParams lp) { - ViewHolder vh = getChildViewHolderInt(child); - if (vh.isRemoved()) { - mRecyclerView.mViewInfoStore.addToDisappearedInLayout(vh); - } else { - mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(vh); - } - mChildHelper.attachViewToParent(child, index, lp, vh.isRemoved()); - if (DISPATCH_TEMP_DETACH) { - ViewCompat.dispatchFinishTemporaryDetach(child); - } - } + int mPrefetchMaxCountObserved; /** - * Reattach a previously {@link #detachView(android.view.View) detached} view. - * This method should not be used to reattach views that were previously - * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. - * - * @param child Child to reattach - * @param index Intended child index for child + * If true, mPrefetchMaxCountObserved is only valid until next layout, and should be reset. */ - public void attachView(@NonNull View child, int index) { - attachView(child, index, (LayoutParams) child.getLayoutParams()); - } + boolean mPrefetchMaxObservedInInitialPrefetch; /** - * Reattach a previously {@link #detachView(android.view.View) detached} view. - * This method should not be used to reattach views that were previously - * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. - * - * @param child Child to reattach + * These measure specs might be the measure specs that were passed into RecyclerView's + * onMeasure method OR fake measure specs created by the RecyclerView. + * For example, when a layout is run, RecyclerView always sets these specs to be + * EXACTLY because a LayoutManager cannot resize RecyclerView during a layout pass. + *

    + * Also, to be able to use the hint in unspecified measure specs, RecyclerView checks the + * API level and sets the size to 0 pre-M to avoid any issue that might be caused by + * corrupt values. Older platforms have no responsibility to provide a size if they set + * mode to unspecified. */ - public void attachView(@NonNull View child) { - attachView(child, -1); - } + private int mWidthMode, mHeightMode; + private int mWidth, mHeight; - /** - * Finish removing a view that was previously temporarily - * {@link #detachView(android.view.View) detached}. - * - * @param child Detached child to remove - */ - public void removeDetachedView(@NonNull View child) { - mRecyclerView.removeDetachedView(child, false); - } /** - * Moves a View from one position to another. + * Interface for LayoutManagers to request items to be prefetched, based on position, with + * specified distance from viewport, which indicates priority. * - * @param fromIndex The View's initial index - * @param toIndex The View's target index + * @see LayoutManager#collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry) + * @see LayoutManager#collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) */ - public void moveView(int fromIndex, int toIndex) { - View view = getChildAt(fromIndex); - if (view == null) { - throw new IllegalArgumentException("Cannot move a child from non-existing index:" - + fromIndex + mRecyclerView.toString()); - } - detachViewAt(fromIndex); - attachView(view, toIndex); + public interface LayoutPrefetchRegistry { + /** + * Requests an an item to be prefetched, based on position, with a specified distance, + * indicating priority. + * + * @param layoutPosition Position of the item to prefetch. + * @param pixelDistance Distance from the current viewport to the bounds of the item, + * must be non-negative. + */ + void addPosition(int layoutPosition, int pixelDistance); } - /** - * Detach a child view and add it to a {@link Recycler Recycler's} scrap heap. - * - *

    Scrapping a view allows it to be rebound and reused to show updated or - * different data.

    - * - * @param child Child to detach and scrap - * @param recycler Recycler to deposit the new scrap view into - */ - public void detachAndScrapView(@NonNull View child, @NonNull Recycler recycler) { - int index = mChildHelper.indexOfChild(child); - scrapOrRecycleView(recycler, index, child); + void setRecyclerView(RecyclerView recyclerView) { + if (recyclerView == null) { + mRecyclerView = null; + mChildHelper = null; + mWidth = 0; + mHeight = 0; + } else { + mRecyclerView = recyclerView; + mChildHelper = recyclerView.mChildHelper; + mWidth = recyclerView.getWidth(); + mHeight = recyclerView.getHeight(); + } + mWidthMode = MeasureSpec.EXACTLY; + mHeightMode = MeasureSpec.EXACTLY; } - /** - * Detach a child view and add it to a {@link Recycler Recycler's} scrap heap. - * - *

    Scrapping a view allows it to be rebound and reused to show updated or - * different data.

    - * - * @param index Index of child to detach and scrap - * @param recycler Recycler to deposit the new scrap view into - */ - public void detachAndScrapViewAt(int index, @NonNull Recycler recycler) { - View child = getChildAt(index); - scrapOrRecycleView(recycler, index, child); - } + void setMeasureSpecs(int wSpec, int hSpec) { + mWidth = MeasureSpec.getSize(wSpec); + mWidthMode = MeasureSpec.getMode(wSpec); + if (mWidthMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) { + mWidth = 0; + } - /** - * Remove a child view and recycle it using the given Recycler. - * - * @param child Child to remove and recycle - * @param recycler Recycler to use to recycle child - */ - public void removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler) { - removeView(child); - recycler.recycleView(child); + mHeight = MeasureSpec.getSize(hSpec); + mHeightMode = MeasureSpec.getMode(hSpec); + if (mHeightMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) { + mHeight = 0; + } } /** - * Remove a child view and recycle it using the given Recycler. + * Called after a layout is calculated during a measure pass when using auto-measure. + *

    + * It simply traverses all children to calculate a bounding box then calls + * {@link #setMeasuredDimension(Rect, int, int)}. LayoutManagers can override that method + * if they need to handle the bounding box differently. + *

    + * For example, GridLayoutManager override that method to ensure that even if a column is + * empty, the GridLayoutManager still measures wide enough to include it. * - * @param index Index of child to remove and recycle - * @param recycler Recycler to use to recycle child + * @param widthSpec The widthSpec that was passing into RecyclerView's onMeasure + * @param heightSpec The heightSpec that was passing into RecyclerView's onMeasure */ - public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) { - View view = getChildAt(index); - removeViewAt(index); - recycler.recycleView(view); + void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) { + final int count = getChildCount(); + if (count == 0) { + mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); + return; + } + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + final Rect bounds = mRecyclerView.mTempRect; + getDecoratedBoundsWithMargins(child, bounds); + if (bounds.left < minX) { + minX = bounds.left; + } + if (bounds.right > maxX) { + maxX = bounds.right; + } + if (bounds.top < minY) { + minY = bounds.top; + } + if (bounds.bottom > maxY) { + maxY = bounds.bottom; + } + } + mRecyclerView.mTempRect.set(minX, minY, maxX, maxY); + setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec); } /** - * Return the current number of child views attached to the parent RecyclerView. - * This does not include child views that were temporarily detached and/or scrapped. + * Sets the measured dimensions from the given bounding box of the children and the + * measurement specs that were passed into {@link RecyclerView#onMeasure(int, int)}. It is + * only called if a LayoutManager returns true from + * {@link #isAutoMeasureEnabled()} and it is called after the RecyclerView calls + * {@link LayoutManager#onLayoutChildren(Recycler, State)} in the execution of + * {@link RecyclerView#onMeasure(int, int)}. + *

    + * This method must call {@link #setMeasuredDimension(int, int)}. + *

    + * The default implementation adds the RecyclerView's padding to the given bounding box + * then caps the value to be within the given measurement specs. * - * @return Number of attached children + * @param childrenBounds The bounding box of all children + * @param wSpec The widthMeasureSpec that was passed into the RecyclerView. + * @param hSpec The heightMeasureSpec that was passed into the RecyclerView. + * @see #isAutoMeasureEnabled() + * @see #setMeasuredDimension(int, int) */ - public int getChildCount() { - return mChildHelper != null ? mChildHelper.getChildCount() : 0; + public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { + int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight(); + int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom(); + int width = chooseSize(wSpec, usedWidth, getMinimumWidth()); + int height = chooseSize(hSpec, usedHeight, getMinimumHeight()); + setMeasuredDimension(width, height); } /** - * Return the child view at the given index - * - * @param index Index of child to return - * @return Child view at index + * Calls {@code RecyclerView#requestLayout} on the underlying RecyclerView */ - @Nullable - public View getChildAt(int index) { - return mChildHelper != null ? mChildHelper.getChildAt(index) : null; + public void requestLayout() { + if (mRecyclerView != null) { + mRecyclerView.requestLayout(); + } } /** - * Return the width measurement spec mode that is currently relevant to the LayoutManager. - * - *

    This value is set only if the LayoutManager opts into the AutoMeasure api via - * {@link #setAutoMeasureEnabled(boolean)}. - * - *

    When RecyclerView is running a layout, this value is always set to - * {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode. + * Checks if RecyclerView is in the middle of a layout or scroll and throws an + * {@link IllegalStateException} if it is not. * - * @return Width measure spec mode - * @see View.MeasureSpec#getMode(int) + * @param message The message for the exception. Can be null. + * @see #assertNotInLayoutOrScroll(String) */ - public int getWidthMode() { - return mWidthMode; + public void assertInLayoutOrScroll(String message) { + if (mRecyclerView != null) { + mRecyclerView.assertInLayoutOrScroll(message); + } } /** - * Return the height measurement spec mode that is currently relevant to the LayoutManager. - * - *

    This value is set only if the LayoutManager opts into the AutoMeasure api via - * {@link #setAutoMeasureEnabled(boolean)}. - * - *

    When RecyclerView is running a layout, this value is always set to - * {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode. + * Chooses a size from the given specs and parameters that is closest to the desired size + * and also complies with the spec. * - * @return Height measure spec mode - * @see View.MeasureSpec#getMode(int) + * @param spec The measureSpec + * @param desired The preferred measurement + * @param min The minimum value + * @return A size that fits to the given specs */ - public int getHeightMode() { - return mHeightMode; + public static int chooseSize(int spec, int desired, int min) { + final int mode = View.MeasureSpec.getMode(spec); + final int size = View.MeasureSpec.getSize(spec); + switch (mode) { + case View.MeasureSpec.EXACTLY: + return size; + case View.MeasureSpec.AT_MOST: + return Math.min(size, Math.max(desired, min)); + case View.MeasureSpec.UNSPECIFIED: + default: + return Math.max(desired, min); + } } /** - * Returns the width that is currently relevant to the LayoutManager. - * - *

    This value is usually equal to the laid out width of the {@link RecyclerView} but may - * reflect the current {@link android.view.View.MeasureSpec} width if the - * {@link LayoutManager} is using AutoMeasure and the RecyclerView is in the process of - * measuring. The LayoutManager must always use this method to retrieve the width relevant - * to it at any given time. + * Checks if RecyclerView is in the middle of a layout or scroll and throws an + * {@link IllegalStateException} if it is. * - * @return Width in pixels + * @param message The message for the exception. Can be null. + * @see #assertInLayoutOrScroll(String) */ - @Px - public int getWidth() { - return mWidth; + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void assertNotInLayoutOrScroll(String message) { + if (mRecyclerView != null) { + mRecyclerView.assertNotInLayoutOrScroll(message); + } } /** - * Returns the height that is currently relevant to the LayoutManager. - * - *

    This value is usually equal to the laid out height of the {@link RecyclerView} but may - * reflect the current {@link android.view.View.MeasureSpec} height if the - * {@link LayoutManager} is using AutoMeasure and the RecyclerView is in the process of - * measuring. The LayoutManager must always use this method to retrieve the height relevant - * to it at any given time. + * Defines whether the measuring pass of layout should use the AutoMeasure mechanism of + * {@link RecyclerView} or if it should be done by the LayoutManager's implementation of + * {@link LayoutManager#onMeasure(Recycler, State, int, int)}. * - * @return Height in pixels + * @param enabled True if layout measurement should be done by the + * RecyclerView, false if it should be done by this + * LayoutManager. + * @see #isAutoMeasureEnabled() + * @deprecated Implementors of LayoutManager should define whether or not it uses + * AutoMeasure by overriding {@link #isAutoMeasureEnabled()}. */ - @Px - public int getHeight() { - return mHeight; + @Deprecated + public void setAutoMeasureEnabled(boolean enabled) { + mAutoMeasure = enabled; } /** - * Return the left padding of the parent RecyclerView + * Returns whether the measuring pass of layout should use the AutoMeasure mechanism of + * {@link RecyclerView} or if it should be done by the LayoutManager's implementation of + * {@link LayoutManager#onMeasure(Recycler, State, int, int)}. + *

    + * This method returns false by default (it actually returns the value passed to the + * deprecated {@link #setAutoMeasureEnabled(boolean)}) and should be overridden to return + * true if a LayoutManager wants to be auto measured by the RecyclerView. + *

    + * If this method is overridden to return true, + * {@link LayoutManager#onMeasure(Recycler, State, int, int)} should not be overridden. + *

    + * AutoMeasure is a RecyclerView mechanism that handles the measuring pass of layout in a + * simple and contract satisfying way, including the wrapping of children laid out by + * LayoutManager. Simply put, it handles wrapping children by calling + * {@link LayoutManager#onLayoutChildren(Recycler, State)} during a call to + * {@link RecyclerView#onMeasure(int, int)}, and then calculating desired dimensions based + * on children's dimensions and positions. It does this while supporting all existing + * animation capabilities of the RecyclerView. + *

    + * More specifically: + *

      + *
    1. When {@link RecyclerView#onMeasure(int, int)} is called, if the provided measure + * specs both have a mode of {@link View.MeasureSpec#EXACTLY}, RecyclerView will set its + * measured dimensions accordingly and return, allowing layout to continue as normal + * (Actually, RecyclerView will call + * {@link LayoutManager#onMeasure(Recycler, State, int, int)} for backwards compatibility + * reasons but it should not be overridden if AutoMeasure is being used).
    2. + *
    3. If one of the layout specs is not {@code EXACT}, the RecyclerView will start the + * layout process. It will first process all pending Adapter updates and + * then decide whether to run a predictive layout. If it decides to do so, it will first + * call {@link #onLayoutChildren(Recycler, State)} with {@link State#isPreLayout()} set to + * {@code true}. At this stage, {@link #getWidth()} and {@link #getHeight()} will still + * return the width and height of the RecyclerView as of the last layout calculation. + *

      + * After handling the predictive case, RecyclerView will call + * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to + * {@code true} and {@link State#isPreLayout()} set to {@code false}. The LayoutManager can + * access the measurement specs via {@link #getHeight()}, {@link #getHeightMode()}, + * {@link #getWidth()} and {@link #getWidthMode()}.

    4. + *
    5. After the layout calculation, RecyclerView sets the measured width & height by + * calculating the bounding box for the children (+ RecyclerView's padding). The + * LayoutManagers can override {@link #setMeasuredDimension(Rect, int, int)} to choose + * different values. For instance, GridLayoutManager overrides this value to handle the case + * where if it is vertical and has 3 columns but only 2 items, it should still measure its + * width to fit 3 items, not 2.
    6. + *
    7. Any following calls to {@link RecyclerView#onMeasure(int, int)} will run + * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to + * {@code true} and {@link State#isPreLayout()} set to {@code false}. RecyclerView will + * take care of which views are actually added / removed / moved / changed for animations so + * that the LayoutManager should not worry about them and handle each + * {@link #onLayoutChildren(Recycler, State)} call as if it is the last one.
    8. + *
    9. When measure is complete and RecyclerView's + * {@link #onLayout(boolean, int, int, int, int)} method is called, RecyclerView checks + * whether it already did layout calculations during the measure pass and if so, it re-uses + * that information. It may still decide to call {@link #onLayoutChildren(Recycler, State)} + * if the last measure spec was different from the final dimensions or adapter contents + * have changed between the measure call and the layout call.
    10. + *
    11. Finally, animations are calculated and run as usual.
    12. + *
    * - * @return Padding in pixels + * @return True if the measuring pass of layout should use the AutoMeasure + * mechanism of {@link RecyclerView} or False if it should be done by the + * LayoutManager's implementation of + * {@link LayoutManager#onMeasure(Recycler, State, int, int)}. + * @see #setMeasuredDimension(Rect, int, int) + * @see #onMeasure(Recycler, State, int, int) */ - @Px - public int getPaddingLeft() { - return mRecyclerView != null ? mRecyclerView.getPaddingLeft() : 0; + public boolean isAutoMeasureEnabled() { + return mAutoMeasure; } /** - * Return the top padding of the parent RecyclerView + * Returns whether this LayoutManager supports "predictive item animations". + *

    + * "Predictive item animations" are automatically created animations that show + * where items came from, and where they are going to, as items are added, removed, + * or moved within a layout. + *

    + * A LayoutManager wishing to support predictive item animations must override this + * method to return true (the default implementation returns false) and must obey certain + * behavioral contracts outlined in {@link #onLayoutChildren(Recycler, State)}. + *

    + * Whether item animations actually occur in a RecyclerView is actually determined by both + * the return value from this method and the + * {@link RecyclerView#setItemAnimator(ItemAnimator) ItemAnimator} set on the + * RecyclerView itself. If the RecyclerView has a non-null ItemAnimator but this + * method returns false, then only "simple item animations" will be enabled in the + * RecyclerView, in which views whose position are changing are simply faded in/out. If the + * RecyclerView has a non-null ItemAnimator and this method returns true, then predictive + * item animations will be enabled in the RecyclerView. * - * @return Padding in pixels + * @return true if this LayoutManager supports predictive item animations, false otherwise. */ - @Px - public int getPaddingTop() { - return mRecyclerView != null ? mRecyclerView.getPaddingTop() : 0; + public boolean supportsPredictiveItemAnimations() { + return false; } /** - * Return the right padding of the parent RecyclerView + * Sets whether the LayoutManager should be queried for views outside of + * its viewport while the UI thread is idle between frames. * - * @return Padding in pixels + *

    If enabled, the LayoutManager will be queried for items to inflate/bind in between + * view system traversals on devices running API 21 or greater. Default value is true.

    + * + *

    On platforms API level 21 and higher, the UI thread is idle between passing a frame + * to RenderThread and the starting up its next frame at the next VSync pulse. By + * prefetching out of window views in this time period, delays from inflation and view + * binding are much less likely to cause jank and stuttering during scrolls and flings.

    + * + *

    While prefetch is enabled, it will have the side effect of expanding the effective + * size of the View cache to hold prefetched views.

    + * + * @param enabled True if items should be prefetched in between traversals. + * @see #isItemPrefetchEnabled() */ - @Px - public int getPaddingRight() { - return mRecyclerView != null ? mRecyclerView.getPaddingRight() : 0; + public final void setItemPrefetchEnabled(boolean enabled) { + if (enabled != mItemPrefetchEnabled) { + mItemPrefetchEnabled = enabled; + mPrefetchMaxCountObserved = 0; + if (mRecyclerView != null) { + mRecyclerView.mRecycler.updateViewCacheSize(); + } + } } /** - * Return the bottom padding of the parent RecyclerView + * Sets whether the LayoutManager should be queried for views outside of + * its viewport while the UI thread is idle between frames. * - * @return Padding in pixels + * @return true if item prefetch is enabled, false otherwise + * @see #setItemPrefetchEnabled(boolean) */ - @Px - public int getPaddingBottom() { - return mRecyclerView != null ? mRecyclerView.getPaddingBottom() : 0; + public final boolean isItemPrefetchEnabled() { + return mItemPrefetchEnabled; } /** - * Return the start padding of the parent RecyclerView + * Gather all positions from the LayoutManager to be prefetched, given specified momentum. * - * @return Padding in pixels + *

    If item prefetch is enabled, this method is called in between traversals to gather + * which positions the LayoutManager will soon need, given upcoming movement in subsequent + * traversals.

    + * + *

    The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for + * each item to be prepared, and these positions will have their ViewHolders created and + * bound, if there is sufficient time available, in advance of being needed by a + * scroll or layout.

    + * + * @param dx X movement component. + * @param dy Y movement component. + * @param state State of RecyclerView + * @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into. + * @see #isItemPrefetchEnabled() + * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) */ - @Px - public int getPaddingStart() { - return mRecyclerView != null ? ViewCompat.getPaddingStart(mRecyclerView) : 0; + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void collectAdjacentPrefetchPositions(int dx, int dy, State state, + LayoutPrefetchRegistry layoutPrefetchRegistry) { } /** - * Return the end padding of the parent RecyclerView + * Gather all positions from the LayoutManager to be prefetched in preperation for its + * RecyclerView to come on screen, due to the movement of another, containing RecyclerView. * - * @return Padding in pixels + *

    This method is only called when a RecyclerView is nested in another RecyclerView.

    + * + *

    If item prefetch is enabled for this LayoutManager, as well in another containing + * LayoutManager, this method is called in between draw traversals to gather + * which positions this LayoutManager will first need, once it appears on the screen.

    + * + *

    For example, if this LayoutManager represents a horizontally scrolling list within a + * vertically scrolling LayoutManager, this method would be called when the horizontal list + * is about to come onscreen.

    + * + *

    The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for + * each item to be prepared, and these positions will have their ViewHolders created and + * bound, if there is sufficient time available, in advance of being needed by a + * scroll or layout.

    + * + * @param adapterItemCount number of items in the associated adapter. + * @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into. + * @see #isItemPrefetchEnabled() + * @see #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry) */ - @Px - public int getPaddingEnd() { - return mRecyclerView != null ? ViewCompat.getPaddingEnd(mRecyclerView) : 0; + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void collectInitialPrefetchPositions(int adapterItemCount, + LayoutPrefetchRegistry layoutPrefetchRegistry) { } - /** - * Returns true if the RecyclerView this LayoutManager is bound to has focus. - * - * @return True if the RecyclerView has focus, false otherwise. - * @see View#isFocused() - */ - public boolean isFocused() { - return mRecyclerView != null && mRecyclerView.isFocused(); + void dispatchAttachedToWindow(RecyclerView view) { + mIsAttachedToWindow = true; + onAttachedToWindow(view); + } + + void dispatchDetachedFromWindow(RecyclerView view, Recycler recycler) { + mIsAttachedToWindow = false; + onDetachedFromWindow(view, recycler); } /** - * Returns true if the RecyclerView this LayoutManager is bound to has or contains focus. + * Returns whether LayoutManager is currently attached to a RecyclerView which is attached + * to a window. * - * @return true if the RecyclerView has or contains focus - * @see View#hasFocus() + * @return True if this LayoutManager is controlling a RecyclerView and the RecyclerView + * is attached to window. */ - public boolean hasFocus() { - return mRecyclerView != null && mRecyclerView.hasFocus(); + public boolean isAttachedToWindow() { + return mIsAttachedToWindow; } /** - * Returns the item View which has or contains focus. + * Causes the Runnable to execute on the next animation time step. + * The runnable will be run on the user interface thread. + *

    + * Calling this method when LayoutManager is not attached to a RecyclerView has no effect. * - * @return A direct child of RecyclerView which has focus or contains the focused child. + * @param action The Runnable that will be executed. + * @see #removeCallbacks */ - @Nullable - public View getFocusedChild() { - if (mRecyclerView == null) { - return null; - } - View focused = mRecyclerView.getFocusedChild(); - if (focused == null || mChildHelper.isHidden(focused)) { - return null; + public void postOnAnimation(Runnable action) { + if (mRecyclerView != null) { + ViewCompat.postOnAnimation(mRecyclerView, action); } - return focused; } /** - * Returns the number of items in the adapter bound to the parent RecyclerView. + * Removes the specified Runnable from the message queue. *

    - * Note that this number is not necessarily equal to - * {@link State#getItemCount() State#getItemCount()}. In methods where {@link State} is - * available, you should use {@link State#getItemCount() State#getItemCount()} instead. - * For more details, check the documentation for - * {@link State#getItemCount() State#getItemCount()}. + * Calling this method when LayoutManager is not attached to a RecyclerView has no effect. * - * @return The number of items in the bound adapter - * @see State#getItemCount() + * @param action The Runnable to remove from the message handling queue + * @return true if RecyclerView could ask the Handler to remove the Runnable, + * false otherwise. When the returned value is true, the Runnable + * may or may not have been actually removed from the message queue + * (for instance, if the Runnable was not in the queue already.) + * @see #postOnAnimation */ - public int getItemCount() { - Adapter a = mRecyclerView != null ? mRecyclerView.getAdapter() : null; - return a != null ? a.getItemCount() : 0; + public boolean removeCallbacks(Runnable action) { + if (mRecyclerView != null) { + return mRecyclerView.removeCallbacks(action); + } + return false; } /** - * Offset all child views attached to the parent RecyclerView by dx pixels along - * the horizontal axis. + * Called when this LayoutManager is both attached to a RecyclerView and that RecyclerView + * is attached to a window. + *

    + * If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not + * call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was + * not requested on the RecyclerView while it was detached. + *

    + * Subclass implementations should always call through to the superclass implementation. * - * @param dx Pixels to offset by + * @param view The RecyclerView this LayoutManager is bound to + * @see #onDetachedFromWindow(RecyclerView, Recycler) */ - public void offsetChildrenHorizontal(@Px int dx) { - if (mRecyclerView != null) { - mRecyclerView.offsetChildrenHorizontal(dx); - } + @CallSuper + public void onAttachedToWindow(RecyclerView view) { } /** - * Offset all child views attached to the parent RecyclerView by dy pixels along - * the vertical axis. - * - * @param dy Pixels to offset by + * @deprecated override {@link #onDetachedFromWindow(RecyclerView, Recycler)} */ - public void offsetChildrenVertical(@Px int dy) { - if (mRecyclerView != null) { - mRecyclerView.offsetChildrenVertical(dy); - } + @Deprecated + public void onDetachedFromWindow(RecyclerView view) { + } /** - * Flags a view so that it will not be scrapped or recycled. + * Called when this LayoutManager is detached from its parent RecyclerView or when + * its parent RecyclerView is detached from its window. *

    - * Scope of ignoring a child is strictly restricted to position tracking, scrapping and - * recyling. Methods like {@link #removeAndRecycleAllViews(Recycler)} will ignore the child - * whereas {@link #removeAllViews()} or {@link #offsetChildrenHorizontal(int)} will not - * ignore the child. + * LayoutManager should clear all of its View references as another LayoutManager might be + * assigned to the RecyclerView. *

    - * Before this child can be recycled again, you have to call - * {@link #stopIgnoringView(View)}. + * If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not + * call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was + * not requested on the RecyclerView while it was detached. *

    - * You can call this method only if your LayoutManger is in onLayout or onScroll callback. + * If your LayoutManager has View references that it cleans in on-detach, it should also + * call {@link RecyclerView#requestLayout()} to ensure that it is re-laid out when + * RecyclerView is re-attached. + *

    + * Subclass implementations should always call through to the superclass implementation. * - * @param view View to ignore. - * @see #stopIgnoringView(View) + * @param view The RecyclerView this LayoutManager is bound to + * @param recycler The recycler to use if you prefer to recycle your children instead of + * keeping them around. + * @see #onAttachedToWindow(RecyclerView) */ - public void ignoreView(@NonNull View view) { - if (view.getParent() != mRecyclerView || mRecyclerView.indexOfChild(view) == -1) { - // checking this because calling this method on a recycled or detached view may - // cause loss of state. - throw new IllegalArgumentException("View should be fully attached to be ignored" - + mRecyclerView.exceptionLabel()); - } - ViewHolder vh = getChildViewHolderInt(view); - vh.addFlags(ViewHolder.FLAG_IGNORE); - mRecyclerView.mViewInfoStore.removeViewHolder(vh); + @CallSuper + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onDetachedFromWindow(RecyclerView view, Recycler recycler) { + onDetachedFromWindow(view); } /** - * View can be scrapped and recycled again. - *

    - * Note that calling this method removes all information in the view holder. - *

    - * You can call this method only if your LayoutManger is in onLayout or onScroll callback. + * Check if the RecyclerView is configured to clip child views to its padding. * - * @param view View to ignore. + * @return true if this RecyclerView clips children to its padding, false otherwise */ - public void stopIgnoringView(@NonNull View view) { - ViewHolder vh = getChildViewHolderInt(view); - vh.stopIgnoring(); - vh.resetInternal(); - vh.addFlags(ViewHolder.FLAG_INVALID); + public boolean getClipToPadding() { + return mRecyclerView != null && mRecyclerView.mClipToPadding; } /** - * Temporarily detach and scrap all currently attached child views. Views will be scrapped - * into the given Recycler. The Recycler may prefer to reuse scrap views before - * other views that were previously recycled. + * Lay out all relevant child views from the given adapter. * - * @param recycler Recycler to scrap views into + * The LayoutManager is in charge of the behavior of item animations. By default, + * RecyclerView has a non-null {@link #getItemAnimator() ItemAnimator}, and simple + * item animations are enabled. This means that add/remove operations on the + * adapter will result in animations to add new or appearing items, removed or + * disappearing items, and moved items. If a LayoutManager returns false from + * {@link #supportsPredictiveItemAnimations()}, which is the default, and runs a + * normal layout operation during {@link #onLayoutChildren(Recycler, State)}, the + * RecyclerView will have enough information to run those animations in a simple + * way. For example, the default ItemAnimator, {@link DefaultItemAnimator}, will + * simply fade views in and out, whether they are actually added/removed or whether + * they are moved on or off the screen due to other add/remove operations. + * + *

    A LayoutManager wanting a better item animation experience, where items can be + * animated onto and off of the screen according to where the items exist when they + * are not on screen, then the LayoutManager should return true from + * {@link #supportsPredictiveItemAnimations()} and add additional logic to + * {@link #onLayoutChildren(Recycler, State)}. Supporting predictive animations + * means that {@link #onLayoutChildren(Recycler, State)} will be called twice; + * once as a "pre" layout step to determine where items would have been prior to + * a real layout, and again to do the "real" layout. In the pre-layout phase, + * items will remember their pre-layout positions to allow them to be laid out + * appropriately. Also, {@link LayoutParams#isItemRemoved() removed} items will + * be returned from the scrap to help determine correct placement of other items. + * These removed items should not be added to the child list, but should be used + * to help calculate correct positioning of other views, including views that + * were not previously onscreen (referred to as APPEARING views), but whose + * pre-layout offscreen position can be determined given the extra + * information about the pre-layout removed views.

    + * + *

    The second layout pass is the real layout in which only non-removed views + * will be used. The only additional requirement during this pass is, if + * {@link #supportsPredictiveItemAnimations()} returns true, to note which + * views exist in the child list prior to layout and which are not there after + * layout (referred to as DISAPPEARING views), and to position/layout those views + * appropriately, without regard to the actual bounds of the RecyclerView. This allows + * the animation system to know the location to which to animate these disappearing + * views.

    + * + *

    The default LayoutManager implementations for RecyclerView handle all of these + * requirements for animations already. Clients of RecyclerView can either use one + * of these layout managers directly or look at their implementations of + * onLayoutChildren() to see how they account for the APPEARING and + * DISAPPEARING views.

    + * + * @param recycler Recycler to use for fetching potentially cached views for a + * position + * @param state Transient state of RecyclerView */ - public void detachAndScrapAttachedViews(@NonNull Recycler recycler) { - int childCount = getChildCount(); - for (int i = childCount - 1; i >= 0; i--) { - View v = getChildAt(i); - scrapOrRecycleView(recycler, i, v); - } - } - - private void scrapOrRecycleView(Recycler recycler, int index, View view) { - ViewHolder viewHolder = getChildViewHolderInt(view); - if (viewHolder.shouldIgnore()) { - if (DEBUG) { - Log.d(TAG, "ignoring view " + viewHolder); - } - return; - } - if (viewHolder.isInvalid() && !viewHolder.isRemoved() - && !mRecyclerView.mAdapter.hasStableIds()) { - removeViewAt(index); - recycler.recycleViewHolderInternal(viewHolder); - } else { - detachViewAt(index); - recycler.scrapView(view); - mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); - } + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onLayoutChildren(Recycler recycler, State state) { + Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) "); } - // we may consider making this public - /** - * Recycles the scrapped views. + * Called after a full layout calculation is finished. The layout calculation may include + * multiple {@link #onLayoutChildren(Recycler, State)} calls due to animations or + * layout measurement but it will include only one {@link #onLayoutCompleted(State)} call. + * This method will be called at the end of {@link View#layout(int, int, int, int)} call. *

    - * When a view is detached and removed, it does not trigger a ViewGroup invalidate. This is - * the expected behavior if scrapped views are used for animations. Otherwise, we need to - * call remove and invalidate RecyclerView to ensure UI update. + * This is a good place for the LayoutManager to do some cleanup like pending scroll + * position, saved state etc. * - * @param recycler Recycler + * @param state Transient state of RecyclerView */ - void removeAndRecycleScrapInt(Recycler recycler) { - int scrapCount = recycler.getScrapCount(); - // Loop backward, recycler might be changed by removeDetachedView() - for (int i = scrapCount - 1; i >= 0; i--) { - View scrap = recycler.getScrapViewAt(i); - ViewHolder vh = getChildViewHolderInt(scrap); - if (vh.shouldIgnore()) { - continue; - } - // If the scrap view is animating, we need to cancel them first. If we cancel it - // here, ItemAnimator callback may recycle it which will cause double recycling. - // To avoid this, we mark it as not recyclable before calling the item animator. - // Since removeDetachedView calls a user API, a common mistake (ending animations on - // the view) may recycle it too, so we guard it before we call user APIs. - vh.setIsRecyclable(false); - if (vh.isTmpDetached()) { - mRecyclerView.removeDetachedView(scrap, false); - } - if (mRecyclerView.mItemAnimator != null) { - mRecyclerView.mItemAnimator.endAnimation(vh); - } - vh.setIsRecyclable(true); - recycler.quickRecycleScrapView(scrap); - } - recycler.clearScrap(); - if (scrapCount > 0) { - mRecyclerView.invalidate(); - } + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onLayoutCompleted(State state) { } /** - * Measure a child view using standard measurement policy, taking the padding - * of the parent RecyclerView and any added item decorations into account. + * Create a default LayoutParams object for a child of the RecyclerView. * - *

    If the RecyclerView can be scrolled in either dimension the caller may - * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.

    + *

    LayoutManagers will often want to use a custom LayoutParams type + * to store extra information specific to the layout. Client code should subclass + * {@link RecyclerView.LayoutParams} for this purpose.

    * - * @param child Child view to measure - * @param widthUsed Width in pixels currently consumed by other views, if relevant - * @param heightUsed Height in pixels currently consumed by other views, if relevant + *

    Important: if you use your own custom LayoutParams type + * you must also override + * {@link #checkLayoutParams(LayoutParams)}, + * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and + * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.

    + * + * @return A new LayoutParams for a child view */ - public void measureChild(@NonNull View child, int widthUsed, int heightUsed) { - LayoutParams lp = (LayoutParams) child.getLayoutParams(); - - Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); - widthUsed += insets.left + insets.right; - heightUsed += insets.top + insets.bottom; - int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), - getPaddingLeft() + getPaddingRight() + widthUsed, lp.width, - canScrollHorizontally()); - int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), - getPaddingTop() + getPaddingBottom() + heightUsed, lp.height, - canScrollVertically()); - if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { - child.measure(widthSpec, heightSpec); - } - } + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public abstract LayoutParams generateDefaultLayoutParams(); /** - * RecyclerView internally does its own View measurement caching which should help with - * WRAP_CONTENT. - *

    - * Use this method if the View is already measured once in this layout pass. + * Determines the validity of the supplied LayoutParams object. + * + *

    This should check to make sure that the object is of the correct type + * and all values are within acceptable ranges. The default implementation + * returns true for non-null params.

    + * + * @param lp LayoutParams object to check + * @return true if this LayoutParams object is valid, false otherwise */ - boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) { - return !mMeasurementCacheEnabled - || !isMeasurementUpToDate(child.getMeasuredWidth(), widthSpec, lp.width) - || !isMeasurementUpToDate(child.getMeasuredHeight(), heightSpec, lp.height); + public boolean checkLayoutParams(LayoutParams lp) { + return lp != null; } /** - * RecyclerView internally does its own View measurement caching which should help with - * WRAP_CONTENT. - *

    - * Use this method if the View is not yet measured and you need to decide whether to - * measure this View or not. + * Create a LayoutParams object suitable for this LayoutManager, copying relevant + * values from the supplied LayoutParams object if possible. + * + *

    Important: if you use your own custom LayoutParams type + * you must also override + * {@link #checkLayoutParams(LayoutParams)}, + * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and + * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.

    + * + * @param lp Source LayoutParams object to copy values from + * @return a new LayoutParams object */ - boolean shouldMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) { - return child.isLayoutRequested() - || !mMeasurementCacheEnabled - || !isMeasurementUpToDate(child.getWidth(), widthSpec, lp.width) - || !isMeasurementUpToDate(child.getHeight(), heightSpec, lp.height); + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { + if (lp instanceof LayoutParams) { + return new LayoutParams((LayoutParams) lp); + } else if (lp instanceof MarginLayoutParams) { + return new LayoutParams((MarginLayoutParams) lp); + } else { + return new LayoutParams(lp); + } } /** - * In addition to the View Framework's measurement cache, RecyclerView uses its own - * additional measurement cache for its children to avoid re-measuring them when not - * necessary. It is on by default but it can be turned off via - * {@link #setMeasurementCacheEnabled(boolean)}. + * Create a LayoutParams object suitable for this LayoutManager from + * an inflated layout resource. * - * @return True if measurement cache is enabled, false otherwise. - * @see #setMeasurementCacheEnabled(boolean) + *

    Important: if you use your own custom LayoutParams type + * you must also override + * {@link #checkLayoutParams(LayoutParams)}, + * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and + * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.

    + * + * @param c Context for obtaining styled attributes + * @param attrs AttributeSet describing the supplied arguments + * @return a new LayoutParams object */ - public boolean isMeasurementCacheEnabled() { - return mMeasurementCacheEnabled; + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { + return new LayoutParams(c, attrs); } /** - * Sets whether RecyclerView should use its own measurement cache for the children. This is - * a more aggressive cache than the framework uses. + * Scroll horizontally by dx pixels in screen coordinates and return the distance traveled. + * The default implementation does nothing and returns 0. * - * @param measurementCacheEnabled True to enable the measurement cache, false otherwise. - * @see #isMeasurementCacheEnabled() + * @param dx distance to scroll by in pixels. X increases as scroll position + * approaches the right. + * @param recycler Recycler to use for fetching potentially cached views for a + * position + * @param state Transient state of RecyclerView + * @return The actual distance scrolled. The return value will be negative if dx was + * negative and scrolling proceeeded in that direction. + * Math.abs(result) may be less than dx if a boundary was reached. */ - public void setMeasurementCacheEnabled(boolean measurementCacheEnabled) { - mMeasurementCacheEnabled = measurementCacheEnabled; + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int scrollHorizontallyBy(int dx, Recycler recycler, State state) { + return 0; } /** - * Measure a child view using standard measurement policy, taking the padding - * of the parent RecyclerView, any added item decorations and the child margins - * into account. - * - *

    If the RecyclerView can be scrolled in either dimension the caller may - * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.

    + * Scroll vertically by dy pixels in screen coordinates and return the distance traveled. + * The default implementation does nothing and returns 0. * - * @param child Child view to measure - * @param widthUsed Width in pixels currently consumed by other views, if relevant - * @param heightUsed Height in pixels currently consumed by other views, if relevant + * @param dy distance to scroll in pixels. Y increases as scroll position + * approaches the bottom. + * @param recycler Recycler to use for fetching potentially cached views for a + * position + * @param state Transient state of RecyclerView + * @return The actual distance scrolled. The return value will be negative if dy was + * negative and scrolling proceeeded in that direction. + * Math.abs(result) may be less than dy if a boundary was reached. */ - public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) { - LayoutParams lp = (LayoutParams) child.getLayoutParams(); - - Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); - widthUsed += insets.left + insets.right; - heightUsed += insets.top + insets.bottom; - - int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), - getPaddingLeft() + getPaddingRight() - + lp.leftMargin + lp.rightMargin + widthUsed, lp.width, - canScrollHorizontally()); - int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), - getPaddingTop() + getPaddingBottom() - + lp.topMargin + lp.bottomMargin + heightUsed, lp.height, - canScrollVertically()); - if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { - child.measure(widthSpec, heightSpec); - } + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int scrollVerticallyBy(int dy, Recycler recycler, State state) { + return 0; } /** - * Returns the measured width of the given child, plus the additional size of - * any insets applied by {@link ItemDecoration ItemDecorations}. + * Query if horizontal scrolling is currently supported. The default implementation + * returns false. * - * @param child Child view to query - * @return child's measured width plus ItemDecoration insets - * @see View#getMeasuredWidth() + * @return True if this LayoutManager can scroll the current contents horizontally */ - public int getDecoratedMeasuredWidth(@NonNull View child) { - Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; - return child.getMeasuredWidth() + insets.left + insets.right; + public boolean canScrollHorizontally() { + return false; } /** - * Returns the measured height of the given child, plus the additional size of - * any insets applied by {@link ItemDecoration ItemDecorations}. + * Query if vertical scrolling is currently supported. The default implementation + * returns false. * - * @param child Child view to query - * @return child's measured height plus ItemDecoration insets - * @see View#getMeasuredHeight() + * @return True if this LayoutManager can scroll the current contents vertically */ - public int getDecoratedMeasuredHeight(@NonNull View child) { - Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; - return child.getMeasuredHeight() + insets.top + insets.bottom; + public boolean canScrollVertically() { + return false; } /** - * Lay out the given child view within the RecyclerView using coordinates that - * include any current {@link ItemDecoration ItemDecorations}. + * Scroll to the specified adapter position. * - *

    LayoutManagers should prefer working in sizes and coordinates that include - * item decoration insets whenever possible. This allows the LayoutManager to effectively - * ignore decoration insets within measurement and layout code. See the following - * methods:

    - *
      - *
    • {@link #layoutDecoratedWithMargins(View, int, int, int, int)}
    • - *
    • {@link #getDecoratedBoundsWithMargins(View, Rect)}
    • - *
    • {@link #measureChild(View, int, int)}
    • - *
    • {@link #measureChildWithMargins(View, int, int)}
    • - *
    • {@link #getDecoratedLeft(View)}
    • - *
    • {@link #getDecoratedTop(View)}
    • - *
    • {@link #getDecoratedRight(View)}
    • - *
    • {@link #getDecoratedBottom(View)}
    • - *
    • {@link #getDecoratedMeasuredWidth(View)}
    • - *
    • {@link #getDecoratedMeasuredHeight(View)}
    • - *
    + * Actual position of the item on the screen depends on the LayoutManager implementation. * - * @param child Child to lay out - * @param left Left edge, with item decoration insets included - * @param top Top edge, with item decoration insets included - * @param right Right edge, with item decoration insets included - * @param bottom Bottom edge, with item decoration insets included - * @see View#layout(int, int, int, int) - * @see #layoutDecoratedWithMargins(View, int, int, int, int) + * @param position Scroll to this adapter position. */ - public void layoutDecorated(@NonNull View child, int left, int top, int right, int bottom) { - Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; - child.layout(left + insets.left, top + insets.top, right - insets.right, - bottom - insets.bottom); + public void scrollToPosition(int position) { + if (DEBUG) { + Log.e(TAG, "You MUST implement scrollToPosition. It will soon become abstract"); + } } /** - * Lay out the given child view within the RecyclerView using coordinates that - * include any current {@link ItemDecoration ItemDecorations} and margins. - * - *

    LayoutManagers should prefer working in sizes and coordinates that include - * item decoration insets whenever possible. This allows the LayoutManager to effectively - * ignore decoration insets within measurement and layout code. See the following - * methods:

    - *
      - *
    • {@link #layoutDecorated(View, int, int, int, int)}
    • - *
    • {@link #measureChild(View, int, int)}
    • - *
    • {@link #measureChildWithMargins(View, int, int)}
    • - *
    • {@link #getDecoratedLeft(View)}
    • - *
    • {@link #getDecoratedTop(View)}
    • - *
    • {@link #getDecoratedRight(View)}
    • - *
    • {@link #getDecoratedBottom(View)}
    • - *
    • {@link #getDecoratedMeasuredWidth(View)}
    • - *
    • {@link #getDecoratedMeasuredHeight(View)}
    • - *
    + *

    Smooth scroll to the specified adapter position.

    + *

    To support smooth scrolling, override this method, create your {@link SmoothScroller} + * instance and call {@link #startSmoothScroll(SmoothScroller)}. + *

    * - * @param child Child to lay out - * @param left Left edge, with item decoration insets and left margin included - * @param top Top edge, with item decoration insets and top margin included - * @param right Right edge, with item decoration insets and right margin included - * @param bottom Bottom edge, with item decoration insets and bottom margin included - * @see View#layout(int, int, int, int) - * @see #layoutDecorated(View, int, int, int, int) + * @param recyclerView The RecyclerView to which this layout manager is attached + * @param state Current State of RecyclerView + * @param position Scroll to this adapter position. */ - public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right, - int bottom) { - LayoutParams lp = (LayoutParams) child.getLayoutParams(); - Rect insets = lp.mDecorInsets; - child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin, - right - insets.right - lp.rightMargin, - bottom - insets.bottom - lp.bottomMargin); + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void smoothScrollToPosition(RecyclerView recyclerView, State state, + int position) { + Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling"); } /** - * Calculates the bounding box of the View while taking into account its matrix changes - * (translation, scale etc) with respect to the RecyclerView. - *

    - * If {@code includeDecorInsets} is {@code true}, they are applied first before applying - * the View's matrix so that the decor offsets also go through the same transformation. + * Starts a smooth scroll using the provided {@link SmoothScroller}. * - * @param child The ItemView whose bounding box should be calculated. - * @param includeDecorInsets True if the decor insets should be included in the bounding box - * @param out The rectangle into which the output will be written. + *

    Each instance of SmoothScroller is intended to only be used once. Provide a new + * SmoothScroller instance each time this method is called. + * + *

    Calling this method will cancel any previous smooth scroll request. + * + * @param smoothScroller Instance which defines how smooth scroll should be animated */ - public void getTransformedBoundingBox(@NonNull View child, boolean includeDecorInsets, - @NonNull Rect out) { - if (includeDecorInsets) { - Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; - out.set(-insets.left, -insets.top, - child.getWidth() + insets.right, child.getHeight() + insets.bottom); - } else { - out.set(0, 0, child.getWidth(), child.getHeight()); - } - - if (mRecyclerView != null) { - Matrix childMatrix = child.getMatrix(); - if (childMatrix != null && !childMatrix.isIdentity()) { - RectF tempRectF = mRecyclerView.mTempRectF; - tempRectF.set(out); - childMatrix.mapRect(tempRectF); - out.set( - (int) Math.floor(tempRectF.left), - (int) Math.floor(tempRectF.top), - (int) Math.ceil(tempRectF.right), - (int) Math.ceil(tempRectF.bottom) - ); - } + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void startSmoothScroll(SmoothScroller smoothScroller) { + if (mSmoothScroller != null && smoothScroller != mSmoothScroller + && mSmoothScroller.isRunning()) { + mSmoothScroller.stop(); } - out.offset(child.getLeft(), child.getTop()); + mSmoothScroller = smoothScroller; + mSmoothScroller.start(mRecyclerView, this); } /** - * Returns the bounds of the view including its decoration and margins. - * - * @param view The view element to check - * @param outBounds A rect that will receive the bounds of the element including its - * decoration and margins. + * @return true if RecyclerView is currently in the state of smooth scrolling. */ - public void getDecoratedBoundsWithMargins(@NonNull View view, @NonNull Rect outBounds) { - getDecoratedBoundsWithMarginsInt(view, outBounds); + public boolean isSmoothScrolling() { + return mSmoothScroller != null && mSmoothScroller.isRunning(); } /** - * Returns the left edge of the given child view within its parent, offset by any applied - * {@link ItemDecoration ItemDecorations}. + * Returns the resolved layout direction for this RecyclerView. * - * @param child Child to query - * @return Child left edge with offsets applied - * @see #getLeftDecorationWidth(View) + * @return {@link androidx.core.view.ViewCompat#LAYOUT_DIRECTION_RTL} if the layout + * direction is RTL or returns + * {@link androidx.core.view.ViewCompat#LAYOUT_DIRECTION_LTR} if the layout direction + * is not RTL. */ - public int getDecoratedLeft(@NonNull View child) { - return child.getLeft() - getLeftDecorationWidth(child); + public int getLayoutDirection() { + return ViewCompat.getLayoutDirection(mRecyclerView); } /** - * Returns the top edge of the given child view within its parent, offset by any applied - * {@link ItemDecoration ItemDecorations}. + * Ends all animations on the view created by the {@link ItemAnimator}. * - * @param child Child to query - * @return Child top edge with offsets applied - * @see #getTopDecorationHeight(View) + * @param view The View for which the animations should be ended. + * @see RecyclerView.ItemAnimator#endAnimations() */ - public int getDecoratedTop(@NonNull View child) { - return child.getTop() - getTopDecorationHeight(child); + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void endAnimation(View view) { + if (mRecyclerView.mItemAnimator != null) { + mRecyclerView.mItemAnimator.endAnimation(getChildViewHolderInt(view)); + } } /** - * Returns the right edge of the given child view within its parent, offset by any applied - * {@link ItemDecoration ItemDecorations}. + * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view + * to the layout that is known to be going away, either because it has been + * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the + * visible portion of the container but is being laid out in order to inform RecyclerView + * in how to animate the item out of view. + *

    + * Views added via this method are going to be invisible to LayoutManager after the + * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)} + * or won't be included in {@link #getChildCount()} method. * - * @param child Child to query - * @return Child right edge with offsets applied - * @see #getRightDecorationWidth(View) + * @param child View to add and then remove with animation. */ - public int getDecoratedRight(@NonNull View child) { - return child.getRight() + getRightDecorationWidth(child); + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void addDisappearingView(View child) { + addDisappearingView(child, -1); } /** - * Returns the bottom edge of the given child view within its parent, offset by any applied - * {@link ItemDecoration ItemDecorations}. + * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view + * to the layout that is known to be going away, either because it has been + * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the + * visible portion of the container but is being laid out in order to inform RecyclerView + * in how to animate the item out of view. + *

    + * Views added via this method are going to be invisible to LayoutManager after the + * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)} + * or won't be included in {@link #getChildCount()} method. * - * @param child Child to query - * @return Child bottom edge with offsets applied - * @see #getBottomDecorationHeight(View) + * @param child View to add and then remove with animation. + * @param index Index of the view. */ - public int getDecoratedBottom(@NonNull View child) { - return child.getBottom() + getBottomDecorationHeight(child); + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void addDisappearingView(View child, int index) { + addViewInt(child, index, true); } /** - * Calculates the item decor insets applied to the given child and updates the provided - * Rect instance with the inset values. - *

      - *
    • The Rect's left is set to the total width of left decorations.
    • - *
    • The Rect's top is set to the total height of top decorations.
    • - *
    • The Rect's right is set to the total width of right decorations.
    • - *
    • The Rect's bottom is set to total height of bottom decorations.
    • - *
    - *

    - * Note that item decorations are automatically calculated when one of the LayoutManager's - * measure child methods is called. If you need to measure the child with custom specs via - * {@link View#measure(int, int)}, you can use this method to get decorations. + * Add a view to the currently attached RecyclerView if needed. LayoutManagers should + * use this method to add views obtained from a {@link Recycler} using + * {@link Recycler#getViewForPosition(int)}. * - * @param child The child view whose decorations should be calculated - * @param outRect The Rect to hold result values + * @param child View to add */ - public void calculateItemDecorationsForChild(@NonNull View child, @NonNull Rect outRect) { - if (mRecyclerView == null) { - outRect.set(0, 0, 0, 0); - return; - } - Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); - outRect.set(insets); + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void addView(View child) { + addView(child, -1); } /** - * Returns the total height of item decorations applied to child's top. - *

    - * Note that this value is not updated until the View is measured or - * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * Add a view to the currently attached RecyclerView if needed. LayoutManagers should + * use this method to add views obtained from a {@link Recycler} using + * {@link Recycler#getViewForPosition(int)}. * - * @param child Child to query - * @return The total height of item decorations applied to the child's top. - * @see #getDecoratedTop(View) - * @see #calculateItemDecorationsForChild(View, Rect) + * @param child View to add + * @param index Index to add child at */ - public int getTopDecorationHeight(@NonNull View child) { - return ((LayoutParams) child.getLayoutParams()).mDecorInsets.top; + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void addView(View child, int index) { + addViewInt(child, index, false); + } + + private void addViewInt(View child, int index, boolean disappearing) { + final ViewHolder holder = getChildViewHolderInt(child); + if (disappearing || holder.isRemoved()) { + // these views will be hidden at the end of the layout pass. + mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder); + } else { + // This may look like unnecessary but may happen if layout manager supports + // predictive layouts and adapter removed then re-added the same item. + // In this case, added version will be visible in the post layout (because add is + // deferred) but RV will still bind it to the same View. + // So if a View re-appears in post layout pass, remove it from disappearing list. + mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder); + } + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (holder.wasReturnedFromScrap() || holder.isScrap()) { + if (holder.isScrap()) { + holder.unScrap(); + } else { + holder.clearReturnedFromScrapFlag(); + } + mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false); + if (DISPATCH_TEMP_DETACH) { + ViewCompat.dispatchFinishTemporaryDetach(child); + } + } else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child + // ensure in correct position + int currentIndex = mChildHelper.indexOfChild(child); + if (index == -1) { + index = mChildHelper.getChildCount(); + } + if (currentIndex == -1) { + throw new IllegalStateException("Added View has RecyclerView as parent but" + + " view is not a real child. Unfiltered index:" + + mRecyclerView.indexOfChild(child) + mRecyclerView.exceptionLabel()); + } + if (currentIndex != index) { + mRecyclerView.mLayout.moveView(currentIndex, index); + } + } else { + mChildHelper.addView(child, index, false); + lp.mInsetsDirty = true; + if (mSmoothScroller != null && mSmoothScroller.isRunning()) { + mSmoothScroller.onChildAttachedToWindow(child); + } + } + if (lp.mPendingInvalidate) { + if (DEBUG) { + Log.d(TAG, "consuming pending invalidate on child " + lp.mViewHolder); + } + holder.itemView.invalidate(); + lp.mPendingInvalidate = false; + } } /** - * Returns the total height of item decorations applied to child's bottom. - *

    - * Note that this value is not updated until the View is measured or - * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should + * use this method to completely remove a child view that is no longer needed. + * LayoutManagers should strongly consider recycling removed views using + * {@link Recycler#recycleView(android.view.View)}. * - * @param child Child to query - * @return The total height of item decorations applied to the child's bottom. - * @see #getDecoratedBottom(View) - * @see #calculateItemDecorationsForChild(View, Rect) + * @param child View to remove */ - public int getBottomDecorationHeight(@NonNull View child) { - return ((LayoutParams) child.getLayoutParams()).mDecorInsets.bottom; + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void removeView(View child) { + mChildHelper.removeView(child); } /** - * Returns the total width of item decorations applied to child's left. - *

    - * Note that this value is not updated until the View is measured or - * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should + * use this method to completely remove a child view that is no longer needed. + * LayoutManagers should strongly consider recycling removed views using + * {@link Recycler#recycleView(android.view.View)}. * - * @param child Child to query - * @return The total width of item decorations applied to the child's left. - * @see #getDecoratedLeft(View) - * @see #calculateItemDecorationsForChild(View, Rect) + * @param index Index of the child view to remove */ - public int getLeftDecorationWidth(@NonNull View child) { - return ((LayoutParams) child.getLayoutParams()).mDecorInsets.left; + public void removeViewAt(int index) { + final View child = getChildAt(index); + if (child != null) { + mChildHelper.removeViewAt(index); + } } /** - * Returns the total width of item decorations applied to child's right. - *

    - * Note that this value is not updated until the View is measured or - * {@link #calculateItemDecorationsForChild(View, Rect)} is called. - * - * @param child Child to query - * @return The total width of item decorations applied to the child's right. - * @see #getDecoratedRight(View) - * @see #calculateItemDecorationsForChild(View, Rect) + * Remove all views from the currently attached RecyclerView. This will not recycle + * any of the affected views; the LayoutManager is responsible for doing so if desired. */ - public int getRightDecorationWidth(@NonNull View child) { - return ((LayoutParams) child.getLayoutParams()).mDecorInsets.right; + public void removeAllViews() { + // Only remove non-animating views + final int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + mChildHelper.removeViewAt(i); + } } /** - * Called when searching for a focusable view in the given direction has failed - * for the current content of the RecyclerView. - * - *

    This is the LayoutManager's opportunity to populate views in the given direction - * to fulfill the request if it can. The LayoutManager should attach and return - * the view to be focused, if a focusable view in the given direction is found. - * Otherwise, if all the existing (or the newly populated views) are unfocusable, it returns - * the next unfocusable view to become visible on the screen. This unfocusable view is - * typically the first view that's either partially or fully out of RV's padded bounded - * area in the given direction. The default implementation returns null.

    + * Returns offset of the RecyclerView's text baseline from the its top boundary. * - * @param focused The currently focused view - * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, - * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, - * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} - * or 0 for not applicable - * @param recycler The recycler to use for obtaining views for currently offscreen items - * @param state Transient state of RecyclerView - * @return The chosen view to be focused if a focusable view is found, otherwise an - * unfocusable view to become visible onto the screen, else null. + * @return The offset of the RecyclerView's text baseline from the its top boundary; -1 if + * there is no baseline. */ - @Nullable - public View onFocusSearchFailed(@NonNull View focused, int direction, - @NonNull Recycler recycler, @NonNull State state) { - return null; + public int getBaseline() { + return -1; } /** - * This method gives a LayoutManager an opportunity to intercept the initial focus search - * before the default behavior of {@link FocusFinder} is used. If this method returns - * null FocusFinder will attempt to find a focusable child view. If it fails - * then {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)} - * will be called to give the LayoutManager an opportunity to add new views for items - * that did not have attached views representing them. The LayoutManager should not add - * or remove views from this method. + * Returns the adapter position of the item represented by the given View. This does not + * contain any adapter changes that might have happened after the last layout. * - * @param focused The currently focused view - * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, - * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, - * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} - * @return A descendant view to focus or null to fall back to default behavior. - * The default implementation returns null. + * @param view The view to query + * @return The adapter position of the item which is rendered by this View. */ - @Nullable - public View onInterceptFocusSearch(@NonNull View focused, int direction) { - return null; + public int getPosition(@NonNull View view) { + return ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition(); } /** - * Returns the scroll amount that brings the given rect in child's coordinate system within - * the padded area of RecyclerView. + * Returns the View type defined by the adapter. * - * @param child The direct child making the request. - * @param rect The rectangle in the child's coordinates the child - * wishes to be on the screen. - * @return The array containing the scroll amount in x and y directions that brings the - * given rect into RV's padded area. + * @param view The view to query + * @return The type of the view assigned by the adapter. */ - private int[] getChildRectangleOnScreenScrollAmount(View child, Rect rect) { - int[] out = new int[2]; - int parentLeft = getPaddingLeft(); - int parentTop = getPaddingTop(); - int parentRight = getWidth() - getPaddingRight(); - int parentBottom = getHeight() - getPaddingBottom(); - int childLeft = child.getLeft() + rect.left - child.getScrollX(); - int childTop = child.getTop() + rect.top - child.getScrollY(); - int childRight = childLeft + rect.width(); - int childBottom = childTop + rect.height(); - - int offScreenLeft = Math.min(0, childLeft - parentLeft); - int offScreenTop = Math.min(0, childTop - parentTop); - int offScreenRight = Math.max(0, childRight - parentRight); - int offScreenBottom = Math.max(0, childBottom - parentBottom); - - // Favor the "start" layout direction over the end when bringing one side or the other - // of a large rect into view. If we decide to bring in end because start is already - // visible, limit the scroll such that start won't go out of bounds. - int dx; - if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) { - dx = offScreenRight != 0 ? offScreenRight - : Math.max(offScreenLeft, childRight - parentRight); - } else { - dx = offScreenLeft != 0 ? offScreenLeft - : Math.min(childLeft - parentLeft, offScreenRight); - } - - // Favor bringing the top into view over the bottom. If top is already visible and - // we should scroll to make bottom visible, make sure top does not go out of bounds. - int dy = offScreenTop != 0 ? offScreenTop - : Math.min(childTop - parentTop, offScreenBottom); - out[0] = dx; - out[1] = dy; - return out; + public int getItemViewType(@NonNull View view) { + return getChildViewHolderInt(view).getItemViewType(); } /** - * Called when a child of the RecyclerView wants a particular rectangle to be positioned - * onto the screen. See {@link ViewParent#requestChildRectangleOnScreen(android.view.View, - * android.graphics.Rect, boolean)} for more details. - * - *

    The base implementation will attempt to perform a standard programmatic scroll - * to bring the given rect into view, within the padded area of the RecyclerView.

    + * Traverses the ancestors of the given view and returns the item view that contains it + * and also a direct child of the LayoutManager. + *

    + * Note that this method may return null if the view is a child of the RecyclerView but + * not a child of the LayoutManager (e.g. running a disappear animation). * - * @param child The direct child making the request. - * @param rect The rectangle in the child's coordinates the child - * wishes to be on the screen. - * @param immediate True to forbid animated or delayed scrolling, - * false otherwise - * @return Whether the group scrolled to handle the operation + * @param view The view that is a descendant of the LayoutManager. + * @return The direct child of the LayoutManager which contains the given view or null if + * the provided view is not a descendant of this LayoutManager. + * @see RecyclerView#getChildViewHolder(View) + * @see RecyclerView#findContainingViewHolder(View) */ - public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, - @NonNull View child, @NonNull Rect rect, boolean immediate) { - return requestChildRectangleOnScreen(parent, child, rect, immediate, false); + @Nullable + public View findContainingItemView(@NonNull View view) { + if (mRecyclerView == null) { + return null; + } + View found = mRecyclerView.findContainingItemView(view); + if (found == null) { + return null; + } + if (mChildHelper.isHidden(found)) { + return null; + } + return found; } /** - * Requests that the given child of the RecyclerView be positioned onto the screen. This - * method can be called for both unfocusable and focusable child views. For unfocusable - * child views, focusedChildVisible is typically true in which case, layout manager - * makes the child view visible only if the currently focused child stays in-bounds of RV. + * Finds the view which represents the given adapter position. + *

    + * This method traverses each child since it has no information about child order. + * Override this method to improve performance if your LayoutManager keeps data about + * child views. + *

    + * If a view is ignored via {@link #ignoreView(View)}, it is also ignored by this method. * - * @param parent The parent RecyclerView. - * @param child The direct child making the request. - * @param rect The rectangle in the child's coordinates the child - * wishes to be on the screen. - * @param immediate True to forbid animated or delayed scrolling, - * false otherwise - * @param focusedChildVisible Whether the currently focused view must stay visible. - * @return Whether the group scrolled to handle the operation + * @param position Position of the item in adapter + * @return The child view that represents the given position or null if the position is not + * laid out */ - public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, - @NonNull View child, @NonNull Rect rect, boolean immediate, - boolean focusedChildVisible) { - int[] scrollAmount = getChildRectangleOnScreenScrollAmount(child, rect - ); - int dx = scrollAmount[0]; - int dy = scrollAmount[1]; - if (!focusedChildVisible || isFocusedChildVisibleAfterScrolling(parent, dx, dy)) { - if (dx != 0 || dy != 0) { - if (immediate) { - parent.scrollBy(dx, dy); - } else { - parent.smoothScrollBy(dx, dy); - } - return true; + @Nullable + public View findViewByPosition(int position) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + ViewHolder vh = getChildViewHolderInt(child); + if (vh == null) { + continue; + } + if (vh.getLayoutPosition() == position && !vh.shouldIgnore() + && (mRecyclerView.mState.isPreLayout() || !vh.isRemoved())) { + return child; } } - return false; + return null; } /** - * Returns whether the given child view is partially or fully visible within the padded - * bounded area of RecyclerView, depending on the input parameters. - * A view is partially visible if it has non-zero overlap with RV's padded bounded area. - * If acceptEndPointInclusion flag is set to true, it's also considered partially - * visible if it's located outside RV's bounds and it's hitting either RV's start or end - * bounds. + * Temporarily detach a child view. * - * @param child The child view to be examined. - * @param completelyVisible If true, the method returns true if and only if the - * child is - * completely visible. If false, the method returns true - * if and - * only if the child is only partially visible (that is it - * will - * return false if the child is either completely visible - * or out - * of RV's bounds). - * @param acceptEndPointInclusion If the view's endpoint intersection with RV's start of end - * bounds is enough to consider it partially visible, - * false otherwise. - * @return True if the given child is partially or fully visible, false otherwise. + *

    LayoutManagers may want to perform a lightweight detach operation to rearrange + * views currently attached to the RecyclerView. Generally LayoutManager implementations + * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} + * so that the detached view may be rebound and reused.

    + * + *

    If a LayoutManager uses this method to detach a view, it must + * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach} + * or {@link #removeDetachedView(android.view.View) fully remove} the detached view + * before the LayoutManager entry point method called by RecyclerView returns.

    + * + * @param child Child to detach */ - public boolean isViewPartiallyVisible(@NonNull View child, boolean completelyVisible, - boolean acceptEndPointInclusion) { - int boundsFlag = (ViewBoundsCheck.FLAG_CVS_GT_PVS | ViewBoundsCheck.FLAG_CVS_EQ_PVS - | ViewBoundsCheck.FLAG_CVE_LT_PVE | ViewBoundsCheck.FLAG_CVE_EQ_PVE); - boolean isViewFullyVisible = mHorizontalBoundCheck.isViewWithinBoundFlags(child, - boundsFlag) - && mVerticalBoundCheck.isViewWithinBoundFlags(child, boundsFlag); - if (completelyVisible) { - return isViewFullyVisible; - } else { - return !isViewFullyVisible; + public void detachView(@NonNull View child) { + final int ind = mChildHelper.indexOfChild(child); + if (ind >= 0) { + detachViewInternal(ind, child); } } /** - * Returns whether the currently focused child stays within RV's bounds with the given - * amount of scrolling. + * Temporarily detach a child view. * - * @param parent The parent RecyclerView. - * @param dx The scrolling in x-axis direction to be performed. - * @param dy The scrolling in y-axis direction to be performed. - * @return {@code false} if the focused child is not at least partially visible after - * scrolling or no focused child exists, {@code true} otherwise. + *

    LayoutManagers may want to perform a lightweight detach operation to rearrange + * views currently attached to the RecyclerView. Generally LayoutManager implementations + * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} + * so that the detached view may be rebound and reused.

    + * + *

    If a LayoutManager uses this method to detach a view, it must + * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach} + * or {@link #removeDetachedView(android.view.View) fully remove} the detached view + * before the LayoutManager entry point method called by RecyclerView returns.

    + * + * @param index Index of the child to detach */ - private boolean isFocusedChildVisibleAfterScrolling(RecyclerView parent, int dx, int dy) { - View focusedChild = parent.getFocusedChild(); - if (focusedChild == null) { - return false; - } - int parentLeft = getPaddingLeft(); - int parentTop = getPaddingTop(); - int parentRight = getWidth() - getPaddingRight(); - int parentBottom = getHeight() - getPaddingBottom(); - Rect bounds = mRecyclerView.mTempRect; - getDecoratedBoundsWithMargins(focusedChild, bounds); - - return bounds.left - dx < parentRight && bounds.right - dx > parentLeft - && bounds.top - dy < parentBottom && bounds.bottom - dy > parentTop; + public void detachViewAt(int index) { + detachViewInternal(index, getChildAt(index)); } - /** - * @deprecated Use {@link #onRequestChildFocus(RecyclerView, State, View, View)} - */ - @Deprecated - public boolean onRequestChildFocus(@NonNull RecyclerView parent, @NonNull View child, - @Nullable View focused) { - // eat the request if we are in the middle of a scroll or layout - return isSmoothScrolling() || parent.isComputingLayout(); + private void detachViewInternal(int index, @NonNull View view) { + if (DISPATCH_TEMP_DETACH) { + ViewCompat.dispatchStartTemporaryDetach(view); + } + mChildHelper.detachViewFromParent(index); } /** - * Called when a descendant view of the RecyclerView requests focus. - * - *

    A LayoutManager wishing to keep focused views aligned in a specific - * portion of the view may implement that behavior in an override of this method.

    - * - *

    If the LayoutManager executes different behavior that should override the default - * behavior of scrolling the focused child on screen instead of running alongside it, - * this method should return true.

    + * Reattach a previously {@link #detachView(android.view.View) detached} view. + * This method should not be used to reattach views that were previously + * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. * - * @param parent The RecyclerView hosting this LayoutManager - * @param state Current state of RecyclerView - * @param child Direct child of the RecyclerView containing the newly focused view - * @param focused The newly focused view. This may be the same view as child or it may be - * null - * @return true if the default scroll behavior should be suppressed + * @param child Child to reattach + * @param index Intended child index for child + * @param lp LayoutParams for child */ - public boolean onRequestChildFocus(@NonNull RecyclerView parent, @NonNull State state, - @NonNull View child, @Nullable View focused) { - return onRequestChildFocus(parent, child, focused); + public void attachView(@NonNull View child, int index, LayoutParams lp) { + ViewHolder vh = getChildViewHolderInt(child); + if (vh.isRemoved()) { + mRecyclerView.mViewInfoStore.addToDisappearedInLayout(vh); + } else { + mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(vh); + } + mChildHelper.attachViewToParent(child, index, lp, vh.isRemoved()); + if (DISPATCH_TEMP_DETACH) { + ViewCompat.dispatchFinishTemporaryDetach(child); + } } /** - * Called if the RecyclerView this LayoutManager is bound to has a different adapter set via - * {@link RecyclerView#setAdapter(Adapter)} or - * {@link RecyclerView#swapAdapter(Adapter, boolean)}. The LayoutManager may use this - * opportunity to clear caches and configure state such that it can relayout appropriately - * with the new data and potentially new view types. - * - *

    The default implementation removes all currently attached views.

    + * Reattach a previously {@link #detachView(android.view.View) detached} view. + * This method should not be used to reattach views that were previously + * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. * - * @param oldAdapter The previous adapter instance. Will be null if there was previously no - * adapter. - * @param newAdapter The new adapter instance. Might be null if - * {@link RecyclerView#setAdapter(RecyclerView.Adapter)} is called with - * {@code null}. + * @param child Child to reattach + * @param index Intended child index for child */ - public void onAdapterChanged(@Nullable Adapter oldAdapter, @Nullable Adapter newAdapter) { + public void attachView(@NonNull View child, int index) { + attachView(child, index, (LayoutParams) child.getLayoutParams()); } /** - * Called to populate focusable views within the RecyclerView. - * - *

    The LayoutManager implementation should return true if the default - * behavior of {@link ViewGroup#addFocusables(java.util.ArrayList, int)} should be - * suppressed.

    - * - *

    The default implementation returns false to trigger RecyclerView - * to fall back to the default ViewGroup behavior.

    + * Reattach a previously {@link #detachView(android.view.View) detached} view. + * This method should not be used to reattach views that were previously + * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. * - * @param recyclerView The RecyclerView hosting this LayoutManager - * @param views List of output views. This method should add valid focusable views - * to this list. - * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, - * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, - * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} - * @param focusableMode The type of focusables to be added. - * @return true to suppress the default behavior, false to add default focusables after - * this method returns. - * @see #FOCUSABLES_ALL - * @see #FOCUSABLES_TOUCH_MODE + * @param child Child to reattach */ - public boolean onAddFocusables(@NonNull RecyclerView recyclerView, - @NonNull ArrayList views, int direction, int focusableMode) { - return false; + public void attachView(@NonNull View child) { + attachView(child, -1); } /** - * Called in response to a call to {@link Adapter#notifyDataSetChanged()} or - * {@link RecyclerView#swapAdapter(Adapter, boolean)} ()} and signals that the the entire - * data set has changed. + * Finish removing a view that was previously temporarily + * {@link #detachView(android.view.View) detached}. + * + * @param child Detached child to remove */ - public void onItemsChanged(@NonNull RecyclerView recyclerView) { + public void removeDetachedView(@NonNull View child) { + mRecyclerView.removeDetachedView(child, false); } /** - * Called when items have been added to the adapter. The LayoutManager may choose to - * requestLayout if the inserted items would require refreshing the currently visible set - * of child views. (e.g. currently empty space would be filled by appended items, etc.) + * Moves a View from one position to another. + * + * @param fromIndex The View's initial index + * @param toIndex The View's target index */ - public void onItemsAdded(@NonNull RecyclerView recyclerView, int positionStart, - int itemCount) { + public void moveView(int fromIndex, int toIndex) { + View view = getChildAt(fromIndex); + if (view == null) { + throw new IllegalArgumentException("Cannot move a child from non-existing index:" + + fromIndex + mRecyclerView.toString()); + } + detachViewAt(fromIndex); + attachView(view, toIndex); } /** - * Called when items have been removed from the adapter. + * Detach a child view and add it to a {@link Recycler Recycler's} scrap heap. + * + *

    Scrapping a view allows it to be rebound and reused to show updated or + * different data.

    + * + * @param child Child to detach and scrap + * @param recycler Recycler to deposit the new scrap view into */ - public void onItemsRemoved(@NonNull RecyclerView recyclerView, int positionStart, - int itemCount) { + public void detachAndScrapView(@NonNull View child, @NonNull Recycler recycler) { + int index = mChildHelper.indexOfChild(child); + scrapOrRecycleView(recycler, index, child); } /** - * Called when items have been changed in the adapter. - * To receive payload, override {@link #onItemsUpdated(RecyclerView, int, int, Object)} - * instead, then this callback will not be invoked. + * Detach a child view and add it to a {@link Recycler Recycler's} scrap heap. + * + *

    Scrapping a view allows it to be rebound and reused to show updated or + * different data.

    + * + * @param index Index of child to detach and scrap + * @param recycler Recycler to deposit the new scrap view into */ - public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart, - int itemCount) { + public void detachAndScrapViewAt(int index, @NonNull Recycler recycler) { + final View child = getChildAt(index); + scrapOrRecycleView(recycler, index, child); } /** - * Called when items have been changed in the adapter and with optional payload. - * Default implementation calls {@link #onItemsUpdated(RecyclerView, int, int)}. + * Remove a child view and recycle it using the given Recycler. + * + * @param child Child to remove and recycle + * @param recycler Recycler to use to recycle child */ - public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart, - int itemCount, @Nullable Object payload) { - onItemsUpdated(recyclerView, positionStart, itemCount); + public void removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler) { + removeView(child); + recycler.recycleView(child); } /** - * Called when an item is moved withing the adapter. - *

    - * Note that, an item may also change position in response to another ADD/REMOVE/MOVE - * operation. This callback is only called if and only if {@link Adapter#notifyItemMoved} - * is called. + * Remove a child view and recycle it using the given Recycler. + * + * @param index Index of child to remove and recycle + * @param recycler Recycler to use to recycle child */ - public void onItemsMoved(@NonNull RecyclerView recyclerView, int from, int to, - int itemCount) { - + public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) { + final View view = getChildAt(index); + removeViewAt(index); + recycler.recycleView(view); } - /** - *

    Override this method if you want to support scroll bars.

    - * - *

    Read {@link RecyclerView#computeHorizontalScrollExtent()} for details.

    - * - *

    Default implementation returns 0.

    + * Return the current number of child views attached to the parent RecyclerView. + * This does not include child views that were temporarily detached and/or scrapped. * - * @param state Current state of RecyclerView - * @return The horizontal extent of the scrollbar's thumb - * @see RecyclerView#computeHorizontalScrollExtent() + * @return Number of attached children */ - public int computeHorizontalScrollExtent(@NonNull State state) { - return 0; + public int getChildCount() { + return mChildHelper != null ? mChildHelper.getChildCount() : 0; } /** - *

    Override this method if you want to support scroll bars.

    - * - *

    Read {@link RecyclerView#computeHorizontalScrollOffset()} for details.

    - * - *

    Default implementation returns 0.

    + * Return the child view at the given index * - * @param state Current State of RecyclerView where you can find total item count - * @return The horizontal offset of the scrollbar's thumb - * @see RecyclerView#computeHorizontalScrollOffset() + * @param index Index of child to return + * @return Child view at index */ - public int computeHorizontalScrollOffset(@NonNull State state) { - return 0; + @Nullable + public View getChildAt(int index) { + return mChildHelper != null ? mChildHelper.getChildAt(index) : null; } /** - *

    Override this method if you want to support scroll bars.

    + * Return the width measurement spec mode that is currently relevant to the LayoutManager. * - *

    Read {@link RecyclerView#computeHorizontalScrollRange()} for details.

    + *

    This value is set only if the LayoutManager opts into the AutoMeasure api via + * {@link #setAutoMeasureEnabled(boolean)}. * - *

    Default implementation returns 0.

    + *

    When RecyclerView is running a layout, this value is always set to + * {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode. * - * @param state Current State of RecyclerView where you can find total item count - * @return The total horizontal range represented by the vertical scrollbar - * @see RecyclerView#computeHorizontalScrollRange() + * @return Width measure spec mode + * @see View.MeasureSpec#getMode(int) */ - public int computeHorizontalScrollRange(@NonNull State state) { - return 0; + public int getWidthMode() { + return mWidthMode; } /** - *

    Override this method if you want to support scroll bars.

    + * Return the height measurement spec mode that is currently relevant to the LayoutManager. * - *

    Read {@link RecyclerView#computeVerticalScrollExtent()} for details.

    + *

    This value is set only if the LayoutManager opts into the AutoMeasure api via + * {@link #setAutoMeasureEnabled(boolean)}. * - *

    Default implementation returns 0.

    + *

    When RecyclerView is running a layout, this value is always set to + * {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode. * - * @param state Current state of RecyclerView - * @return The vertical extent of the scrollbar's thumb - * @see RecyclerView#computeVerticalScrollExtent() + * @return Height measure spec mode + * @see View.MeasureSpec#getMode(int) */ - public int computeVerticalScrollExtent(@NonNull State state) { - return 0; + public int getHeightMode() { + return mHeightMode; } /** - *

    Override this method if you want to support scroll bars.

    - * - *

    Read {@link RecyclerView#computeVerticalScrollOffset()} for details.

    + * Returns the width that is currently relevant to the LayoutManager. * - *

    Default implementation returns 0.

    + *

    This value is usually equal to the laid out width of the {@link RecyclerView} but may + * reflect the current {@link android.view.View.MeasureSpec} width if the + * {@link LayoutManager} is using AutoMeasure and the RecyclerView is in the process of + * measuring. The LayoutManager must always use this method to retrieve the width relevant + * to it at any given time. * - * @param state Current State of RecyclerView where you can find total item count - * @return The vertical offset of the scrollbar's thumb - * @see RecyclerView#computeVerticalScrollOffset() + * @return Width in pixels */ - public int computeVerticalScrollOffset(@NonNull State state) { - return 0; + @Px + public int getWidth() { + return mWidth; } /** - *

    Override this method if you want to support scroll bars.

    - * - *

    Read {@link RecyclerView#computeVerticalScrollRange()} for details.

    + * Returns the height that is currently relevant to the LayoutManager. * - *

    Default implementation returns 0.

    + *

    This value is usually equal to the laid out height of the {@link RecyclerView} but may + * reflect the current {@link android.view.View.MeasureSpec} height if the + * {@link LayoutManager} is using AutoMeasure and the RecyclerView is in the process of + * measuring. The LayoutManager must always use this method to retrieve the height relevant + * to it at any given time. * - * @param state Current State of RecyclerView where you can find total item count - * @return The total vertical range represented by the vertical scrollbar - * @see RecyclerView#computeVerticalScrollRange() + * @return Height in pixels */ - public int computeVerticalScrollRange(@NonNull State state) { - return 0; + @Px + public int getHeight() { + return mHeight; } /** - * Measure the attached RecyclerView. Implementations must call - * {@link #setMeasuredDimension(int, int)} before returning. - *

    - * It is strongly advised to use the AutoMeasure mechanism by overriding - * {@link #isAutoMeasureEnabled()} to return true as AutoMeasure handles all the standard - * measure cases including when the RecyclerView's layout_width or layout_height have been - * set to wrap_content. If {@link #isAutoMeasureEnabled()} is overridden to return true, - * this method should not be overridden. - *

    - * The default implementation will handle EXACTLY measurements and respect - * the minimum width and height properties of the host RecyclerView if measured - * as UNSPECIFIED. AT_MOST measurements will be treated as EXACTLY and the RecyclerView - * will consume all available space. + * Return the left padding of the parent RecyclerView * - * @param recycler Recycler - * @param state Transient state of RecyclerView - * @param widthSpec Width {@link android.view.View.MeasureSpec} - * @param heightSpec Height {@link android.view.View.MeasureSpec} - * @see #isAutoMeasureEnabled() - * @see #setMeasuredDimension(int, int) + * @return Padding in pixels */ - public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec, - int heightSpec) { - mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); + @Px + public int getPaddingLeft() { + return mRecyclerView != null ? mRecyclerView.getPaddingLeft() : 0; } /** - * {@link View#setMeasuredDimension(int, int) Set the measured dimensions} of the - * host RecyclerView. + * Return the top padding of the parent RecyclerView * - * @param widthSize Measured width - * @param heightSize Measured height + * @return Padding in pixels */ - public void setMeasuredDimension(int widthSize, int heightSize) { - mRecyclerView.setMeasuredDimension(widthSize, heightSize); + @Px + public int getPaddingTop() { + return mRecyclerView != null ? mRecyclerView.getPaddingTop() : 0; } /** - * @return The host RecyclerView's {@link View#getMinimumWidth()} + * Return the right padding of the parent RecyclerView + * + * @return Padding in pixels */ @Px - public int getMinimumWidth() { - return ViewCompat.getMinimumWidth(mRecyclerView); + public int getPaddingRight() { + return mRecyclerView != null ? mRecyclerView.getPaddingRight() : 0; } /** - * @return The host RecyclerView's {@link View#getMinimumHeight()} + * Return the bottom padding of the parent RecyclerView + * + * @return Padding in pixels */ @Px - public int getMinimumHeight() { - return ViewCompat.getMinimumHeight(mRecyclerView); + public int getPaddingBottom() { + return mRecyclerView != null ? mRecyclerView.getPaddingBottom() : 0; } /** - *

    Called when the LayoutManager should save its state. This is a good time to save your - * scroll position, configuration and anything else that may be required to restore the same - * layout state if the LayoutManager is recreated.

    - *

    RecyclerView does NOT verify if the LayoutManager has changed between state save and - * restore. This will let you share information between your LayoutManagers but it is also - * your responsibility to make sure they use the same parcelable class.

    + * Return the start padding of the parent RecyclerView * - * @return Necessary information for LayoutManager to be able to restore its state + * @return Padding in pixels */ - @Nullable - public Parcelable onSaveInstanceState() { - return null; + @Px + public int getPaddingStart() { + return mRecyclerView != null ? ViewCompat.getPaddingStart(mRecyclerView) : 0; } /** - * Called when the RecyclerView is ready to restore the state based on a previous - * RecyclerView. - *

    - * Notice that this might happen after an actual layout, based on how Adapter prefers to - * restore State. See {@link Adapter#getStateRestorationPolicy()} for more information. + * Return the end padding of the parent RecyclerView * - * @param state The parcelable that was returned by the previous LayoutManager's - * {@link #onSaveInstanceState()} method. + * @return Padding in pixels */ - @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly - public void onRestoreInstanceState(Parcelable state) { - - } - - void stopSmoothScroller() { - if (mSmoothScroller != null) { - mSmoothScroller.stop(); - } - } - - void onSmoothScrollerStopped(SmoothScroller smoothScroller) { - if (mSmoothScroller == smoothScroller) { - mSmoothScroller = null; - } + @Px + public int getPaddingEnd() { + return mRecyclerView != null ? ViewCompat.getPaddingEnd(mRecyclerView) : 0; } /** - * RecyclerView calls this method to notify LayoutManager that scroll state has changed. + * Returns true if the RecyclerView this LayoutManager is bound to has focus. * - * @param state The new scroll state for RecyclerView + * @return True if the RecyclerView has focus, false otherwise. + * @see View#isFocused() */ - public void onScrollStateChanged(int state) { + public boolean isFocused() { + return mRecyclerView != null && mRecyclerView.isFocused(); } /** - * Removes all views and recycles them using the given recycler. - *

    - * If you want to clean cached views as well, you should call {@link Recycler#clear()} too. - *

    - * If a View is marked as "ignored", it is not removed nor recycled. + * Returns true if the RecyclerView this LayoutManager is bound to has or contains focus. * - * @param recycler Recycler to use to recycle children - * @see #removeAndRecycleView(View, Recycler) - * @see #removeAndRecycleViewAt(int, Recycler) - * @see #ignoreView(View) + * @return true if the RecyclerView has or contains focus + * @see View#hasFocus() */ - public void removeAndRecycleAllViews(@NonNull Recycler recycler) { - for (int i = getChildCount() - 1; i >= 0; i--) { - View view = getChildAt(i); - if (!getChildViewHolderInt(view).shouldIgnore()) { - removeAndRecycleViewAt(i, recycler); - } - } - } - - // called by accessibility delegate - void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfoCompat info) { - onInitializeAccessibilityNodeInfo(mRecyclerView.mRecycler, mRecyclerView.mState, info); + public boolean hasFocus() { + return mRecyclerView != null && mRecyclerView.hasFocus(); } /** - * Called by the AccessibilityDelegate when the information about the current layout should - * be populated. - *

    - * Default implementation adds a {@link - * androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat}. - *

    - * You should override - * {@link #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)}, - * {@link #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)}, - * {@link #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State)} and - * {@link #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State)} for - * more accurate accessibility information. + * Returns the item View which has or contains focus. * - * @param recycler The Recycler that can be used to convert view positions into adapter - * positions - * @param state The current state of RecyclerView - * @param info The info that should be filled by the LayoutManager - * @see View#onInitializeAccessibilityNodeInfo( - *android.view.accessibility.AccessibilityNodeInfo) - * @see #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State) - * @see #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State) - * @see #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State) - * @see #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State) + * @return A direct child of RecyclerView which has focus or contains the focused child. */ - public void onInitializeAccessibilityNodeInfo(@NonNull Recycler recycler, - @NonNull State state, @NonNull AccessibilityNodeInfoCompat info) { - if (mRecyclerView.canScrollVertically(-1) || mRecyclerView.canScrollHorizontally(-1)) { - info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); - info.setScrollable(true); + @Nullable + public View getFocusedChild() { + if (mRecyclerView == null) { + return null; } - if (mRecyclerView.canScrollVertically(1) || mRecyclerView.canScrollHorizontally(1)) { - info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); - info.setScrollable(true); + final View focused = mRecyclerView.getFocusedChild(); + if (focused == null || mChildHelper.isHidden(focused)) { + return null; } - AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo = - AccessibilityNodeInfoCompat.CollectionInfoCompat - .obtain(getRowCountForAccessibility(recycler, state), - getColumnCountForAccessibility(recycler, state), - isLayoutHierarchical(recycler, state), - getSelectionModeForAccessibility(recycler, state)); - info.setCollectionInfo(collectionInfo); - } - - // called by accessibility delegate - public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) { - onInitializeAccessibilityEvent(mRecyclerView.mRecycler, mRecyclerView.mState, event); + return focused; } /** - * Called by the accessibility delegate to initialize an accessibility event. + * Returns the number of items in the adapter bound to the parent RecyclerView. *

    - * Default implementation adds item count and scroll information to the event. + * Note that this number is not necessarily equal to + * {@link State#getItemCount() State#getItemCount()}. In methods where {@link State} is + * available, you should use {@link State#getItemCount() State#getItemCount()} instead. + * For more details, check the documentation for + * {@link State#getItemCount() State#getItemCount()}. * - * @param recycler The Recycler that can be used to convert view positions into adapter - * positions - * @param state The current state of RecyclerView - * @param event The event instance to initialize - * @see View#onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent) + * @return The number of items in the bound adapter + * @see State#getItemCount() */ - public void onInitializeAccessibilityEvent(@NonNull Recycler recycler, @NonNull State state, - @NonNull AccessibilityEvent event) { - if (mRecyclerView == null || event == null) { - return; - } - event.setScrollable(mRecyclerView.canScrollVertically(1) - || mRecyclerView.canScrollVertically(-1) - || mRecyclerView.canScrollHorizontally(-1) - || mRecyclerView.canScrollHorizontally(1)); + public int getItemCount() { + final Adapter a = mRecyclerView != null ? mRecyclerView.getAdapter() : null; + return a != null ? a.getItemCount() : 0; + } - if (mRecyclerView.mAdapter != null) { - event.setItemCount(mRecyclerView.mAdapter.getItemCount()); + /** + * Offset all child views attached to the parent RecyclerView by dx pixels along + * the horizontal axis. + * + * @param dx Pixels to offset by + */ + public void offsetChildrenHorizontal(@Px int dx) { + if (mRecyclerView != null) { + mRecyclerView.offsetChildrenHorizontal(dx); } } - // called by accessibility delegate - void onInitializeAccessibilityNodeInfoForItem(View host, AccessibilityNodeInfoCompat info) { - ViewHolder vh = getChildViewHolderInt(host); - // avoid trying to create accessibility node info for removed children - if (vh != null && !vh.isRemoved() && !mChildHelper.isHidden(vh.itemView)) { - onInitializeAccessibilityNodeInfoForItem(mRecyclerView.mRecycler, - mRecyclerView.mState, host, info); + /** + * Offset all child views attached to the parent RecyclerView by dy pixels along + * the vertical axis. + * + * @param dy Pixels to offset by + */ + public void offsetChildrenVertical(@Px int dy) { + if (mRecyclerView != null) { + mRecyclerView.offsetChildrenVertical(dy); } } /** - * Called by the AccessibilityDelegate when the accessibility information for a specific - * item should be populated. + * Flags a view so that it will not be scrapped or recycled. *

    - * Default implementation adds basic positioning information about the item. + * Scope of ignoring a child is strictly restricted to position tracking, scrapping and + * recyling. Methods like {@link #removeAndRecycleAllViews(Recycler)} will ignore the child + * whereas {@link #removeAllViews()} or {@link #offsetChildrenHorizontal(int)} will not + * ignore the child. + *

    + * Before this child can be recycled again, you have to call + * {@link #stopIgnoringView(View)}. + *

    + * You can call this method only if your LayoutManger is in onLayout or onScroll callback. * - * @param recycler The Recycler that can be used to convert view positions into adapter - * positions - * @param state The current state of RecyclerView - * @param host The child for which accessibility node info should be populated - * @param info The info to fill out about the item - * @see android.widget.AbsListView#onInitializeAccessibilityNodeInfoForItem(View, int, - * android.view.accessibility.AccessibilityNodeInfo) + * @param view View to ignore. + * @see #stopIgnoringView(View) */ - public void onInitializeAccessibilityNodeInfoForItem(@NonNull Recycler recycler, - @NonNull State state, @NonNull View host, - @NonNull AccessibilityNodeInfoCompat info) { + public void ignoreView(@NonNull View view) { + if (view.getParent() != mRecyclerView || mRecyclerView.indexOfChild(view) == -1) { + // checking this because calling this method on a recycled or detached view may + // cause loss of state. + throw new IllegalArgumentException("View should be fully attached to be ignored" + + mRecyclerView.exceptionLabel()); + } + final ViewHolder vh = getChildViewHolderInt(view); + vh.addFlags(ViewHolder.FLAG_IGNORE); + mRecyclerView.mViewInfoStore.removeViewHolder(vh); } /** - * A LayoutManager can call this method to force RecyclerView to run simple animations in - * the next layout pass, even if there is not any trigger to do so. (e.g. adapter data - * change). + * View can be scrapped and recycled again. *

    - * Note that, calling this method will not guarantee that RecyclerView will run animations - * at all. For example, if there is not any {@link ItemAnimator} set, RecyclerView will - * not run any animations but will still clear this flag after the layout is complete. + * Note that calling this method removes all information in the view holder. + *

    + * You can call this method only if your LayoutManger is in onLayout or onScroll callback. + * + * @param view View to ignore. */ - public void requestSimpleAnimationsInNextLayout() { - mRequestedSimpleAnimations = true; + public void stopIgnoringView(@NonNull View view) { + final ViewHolder vh = getChildViewHolderInt(view); + vh.stopIgnoring(); + vh.resetInternal(); + vh.addFlags(ViewHolder.FLAG_INVALID); } /** - * Returns the selection mode for accessibility. Should be - * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE}, - * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_SINGLE} or - * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_MULTIPLE}. - *

    - * Default implementation returns - * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE}. + * Temporarily detach and scrap all currently attached child views. Views will be scrapped + * into the given Recycler. The Recycler may prefer to reuse scrap views before + * other views that were previously recycled. * - * @param recycler The Recycler that can be used to convert view positions into adapter - * positions - * @param state The current state of RecyclerView - * @return Selection mode for accessibility. Default implementation returns - * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE}. + * @param recycler Recycler to scrap views into */ - public int getSelectionModeForAccessibility(@NonNull Recycler recycler, - @NonNull State state) { - return AccessibilityNodeInfoCompat.CollectionInfoCompat.SELECTION_MODE_NONE; + public void detachAndScrapAttachedViews(@NonNull Recycler recycler) { + final int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + final View v = getChildAt(i); + scrapOrRecycleView(recycler, i, v); + } + } + + private void scrapOrRecycleView(Recycler recycler, int index, View view) { + final ViewHolder viewHolder = getChildViewHolderInt(view); + if (viewHolder.shouldIgnore()) { + if (DEBUG) { + Log.d(TAG, "ignoring view " + viewHolder); + } + return; + } + if (viewHolder.isInvalid() && !viewHolder.isRemoved() + && !mRecyclerView.mAdapter.hasStableIds()) { + removeViewAt(index); + recycler.recycleViewHolderInternal(viewHolder); + } else { + detachViewAt(index); + recycler.scrapView(view); + mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); + } } /** - * Returns the number of rows for accessibility. + * Recycles the scrapped views. *

    - * Default implementation returns the number of items in the adapter if LayoutManager - * supports vertical scrolling or 1 if LayoutManager does not support vertical - * scrolling. + * When a view is detached and removed, it does not trigger a ViewGroup invalidate. This is + * the expected behavior if scrapped views are used for animations. Otherwise, we need to + * call remove and invalidate RecyclerView to ensure UI update. * - * @param recycler The Recycler that can be used to convert view positions into adapter - * positions - * @param state The current state of RecyclerView - * @return The number of rows in LayoutManager for accessibility. + * @param recycler Recycler */ - public int getRowCountForAccessibility(@NonNull Recycler recycler, @NonNull State state) { - return -1; + void removeAndRecycleScrapInt(Recycler recycler) { + final int scrapCount = recycler.getScrapCount(); + // Loop backward, recycler might be changed by removeDetachedView() + for (int i = scrapCount - 1; i >= 0; i--) { + final View scrap = recycler.getScrapViewAt(i); + final ViewHolder vh = getChildViewHolderInt(scrap); + if (vh.shouldIgnore()) { + continue; + } + // If the scrap view is animating, we need to cancel them first. If we cancel it + // here, ItemAnimator callback may recycle it which will cause double recycling. + // To avoid this, we mark it as not recyclable before calling the item animator. + // Since removeDetachedView calls a user API, a common mistake (ending animations on + // the view) may recycle it too, so we guard it before we call user APIs. + vh.setIsRecyclable(false); + if (vh.isTmpDetached()) { + mRecyclerView.removeDetachedView(scrap, false); + } + if (mRecyclerView.mItemAnimator != null) { + mRecyclerView.mItemAnimator.endAnimation(vh); + } + vh.setIsRecyclable(true); + recycler.quickRecycleScrapView(scrap); + } + recycler.clearScrap(); + if (scrapCount > 0) { + mRecyclerView.invalidate(); + } } + /** - * Returns the number of columns for accessibility. - *

    - * Default implementation returns the number of items in the adapter if LayoutManager - * supports horizontal scrolling or 1 if LayoutManager does not support horizontal - * scrolling. + * Measure a child view using standard measurement policy, taking the padding + * of the parent RecyclerView and any added item decorations into account. * - * @param recycler The Recycler that can be used to convert view positions into adapter - * positions - * @param state The current state of RecyclerView - * @return The number of rows in LayoutManager for accessibility. + *

    If the RecyclerView can be scrolled in either dimension the caller may + * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.

    + * + * @param child Child view to measure + * @param widthUsed Width in pixels currently consumed by other views, if relevant + * @param heightUsed Height in pixels currently consumed by other views, if relevant */ - public int getColumnCountForAccessibility(@NonNull Recycler recycler, - @NonNull State state) { - return -1; + public void measureChild(@NonNull View child, int widthUsed, int heightUsed) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); + widthUsed += insets.left + insets.right; + heightUsed += insets.top + insets.bottom; + final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), + getPaddingLeft() + getPaddingRight() + widthUsed, lp.width, + canScrollHorizontally()); + final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), + getPaddingTop() + getPaddingBottom() + heightUsed, lp.height, + canScrollVertically()); + if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { + child.measure(widthSpec, heightSpec); + } } /** - * Returns whether layout is hierarchical or not to be used for accessibility. + * RecyclerView internally does its own View measurement caching which should help with + * WRAP_CONTENT. *

    - * Default implementation returns false. - * - * @param recycler The Recycler that can be used to convert view positions into adapter - * positions - * @param state The current state of RecyclerView - * @return True if layout is hierarchical. + * Use this method if the View is already measured once in this layout pass. */ - public boolean isLayoutHierarchical(@NonNull Recycler recycler, @NonNull State state) { - return false; + boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) { + return !mMeasurementCacheEnabled + || !isMeasurementUpToDate(child.getMeasuredWidth(), widthSpec, lp.width) + || !isMeasurementUpToDate(child.getMeasuredHeight(), heightSpec, lp.height); } - // called by accessibility delegate - boolean performAccessibilityAction(int action, @Nullable Bundle args) { - return performAccessibilityAction(mRecyclerView.mRecycler, mRecyclerView.mState, - action, args); - } + // we may consider making this public /** - * Called by AccessibilityDelegate when an action is requested from the RecyclerView. - * - * @param recycler The Recycler that can be used to convert view positions into adapter - * positions - * @param state The current state of RecyclerView - * @param action The action to perform - * @param args Optional action arguments - * @see View#performAccessibilityAction(int, android.os.Bundle) + * RecyclerView internally does its own View measurement caching which should help with + * WRAP_CONTENT. + *

    + * Use this method if the View is not yet measured and you need to decide whether to + * measure this View or not. */ - public boolean performAccessibilityAction(@NonNull Recycler recycler, @NonNull State state, - int action, @Nullable Bundle args) { - if (mRecyclerView == null) { - return false; - } - int vScroll = 0, hScroll = 0; - int height = getHeight(); - int width = getWidth(); - Rect rect = new Rect(); - // Gets the visible rect on the screen except for the rotation or scale cases which - // might affect the result. - if (mRecyclerView.getMatrix().isIdentity() && mRecyclerView.getGlobalVisibleRect( - rect)) { - height = rect.height(); - width = rect.width(); - } - switch (action) { - case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: - if (mRecyclerView.canScrollVertically(-1)) { - vScroll = -(height - getPaddingTop() - getPaddingBottom()); - } - if (mRecyclerView.canScrollHorizontally(-1)) { - hScroll = -(width - getPaddingLeft() - getPaddingRight()); - } - break; - case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: - if (mRecyclerView.canScrollVertically(1)) { - vScroll = height - getPaddingTop() - getPaddingBottom(); - } - if (mRecyclerView.canScrollHorizontally(1)) { - hScroll = width - getPaddingLeft() - getPaddingRight(); - } - break; - } - if (vScroll == 0 && hScroll == 0) { - return false; - } - mRecyclerView.smoothScrollBy(hScroll, vScroll, null, UNDEFINED_DURATION, true); - return true; - } - - // called by accessibility delegate - boolean performAccessibilityActionForItem(@NonNull View view, int action, - @Nullable Bundle args) { - return performAccessibilityActionForItem(mRecyclerView.mRecycler, mRecyclerView.mState, - view, action, args); + boolean shouldMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) { + return child.isLayoutRequested() + || !mMeasurementCacheEnabled + || !isMeasurementUpToDate(child.getWidth(), widthSpec, lp.width) + || !isMeasurementUpToDate(child.getHeight(), heightSpec, lp.height); } /** - * Called by AccessibilityDelegate when an accessibility action is requested on one of the - * children of LayoutManager. - *

    - * Default implementation does not do anything. + * In addition to the View Framework's measurement cache, RecyclerView uses its own + * additional measurement cache for its children to avoid re-measuring them when not + * necessary. It is on by default but it can be turned off via + * {@link #setMeasurementCacheEnabled(boolean)}. * - * @param recycler The Recycler that can be used to convert view positions into adapter - * positions - * @param state The current state of RecyclerView - * @param view The child view on which the action is performed - * @param action The action to perform - * @param args Optional action arguments - * @return true if action is handled - * @see View#performAccessibilityAction(int, android.os.Bundle) + * @return True if measurement cache is enabled, false otherwise. + * @see #setMeasurementCacheEnabled(boolean) */ - public boolean performAccessibilityActionForItem(@NonNull Recycler recycler, - @NonNull State state, @NonNull View view, int action, @Nullable Bundle args) { - return false; - } - - void setExactMeasureSpecsFrom(RecyclerView recyclerView) { - setMeasureSpecs( - MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(recyclerView.getHeight(), MeasureSpec.EXACTLY) - ); + public boolean isMeasurementCacheEnabled() { + return mMeasurementCacheEnabled; } /** - * Internal API to allow LayoutManagers to be measured twice. - *

    - * This is not public because LayoutManagers should be able to handle their layouts in one - * pass but it is very convenient to make existing LayoutManagers support wrapping content - * when both orientations are undefined. - *

    - * This API will be removed after default LayoutManagers properly implement wrap content in - * non-scroll orientation. + * Sets whether RecyclerView should use its own measurement cache for the children. This is + * a more aggressive cache than the framework uses. + * + * @param measurementCacheEnabled True to enable the measurement cache, false otherwise. + * @see #isMeasurementCacheEnabled() */ - boolean shouldMeasureTwice() { - return false; + public void setMeasurementCacheEnabled(boolean measurementCacheEnabled) { + mMeasurementCacheEnabled = measurementCacheEnabled; } - boolean hasFlexibleChildInBothOrientations() { - int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = getChildAt(i); - ViewGroup.LayoutParams lp = child.getLayoutParams(); - if (lp.width < 0 && lp.height < 0) { + private static boolean isMeasurementUpToDate(int childSize, int spec, int dimension) { + final int specMode = MeasureSpec.getMode(spec); + final int specSize = MeasureSpec.getSize(spec); + if (dimension > 0 && childSize != dimension) { + return false; + } + switch (specMode) { + case MeasureSpec.UNSPECIFIED: return true; - } + case MeasureSpec.AT_MOST: + return specSize >= childSize; + case MeasureSpec.EXACTLY: + return specSize == childSize; } return false; } /** - * Interface for LayoutManagers to request items to be prefetched, based on position, with - * specified distance from viewport, which indicates priority. + * Measure a child view using standard measurement policy, taking the padding + * of the parent RecyclerView, any added item decorations and the child margins + * into account. * - * @see LayoutManager#collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry) - * @see LayoutManager#collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + *

    If the RecyclerView can be scrolled in either dimension the caller may + * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.

    + * + * @param child Child view to measure + * @param widthUsed Width in pixels currently consumed by other views, if relevant + * @param heightUsed Height in pixels currently consumed by other views, if relevant */ - public interface LayoutPrefetchRegistry { - /** - * Requests an an item to be prefetched, based on position, with a specified distance, - * indicating priority. - * - * @param layoutPosition Position of the item to prefetch. - * @param pixelDistance Distance from the current viewport to the bounds of the item, - * must be non-negative. - */ - void addPosition(int layoutPosition, int pixelDistance); + public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); + widthUsed += insets.left + insets.right; + heightUsed += insets.top + insets.bottom; + + final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), + getPaddingLeft() + getPaddingRight() + + lp.leftMargin + lp.rightMargin + widthUsed, lp.width, + canScrollHorizontally()); + final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), + getPaddingTop() + getPaddingBottom() + + lp.topMargin + lp.bottomMargin + heightUsed, lp.height, + canScrollVertically()); + if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { + child.measure(widthSpec, heightSpec); + } } /** - * Some general properties that a LayoutManager may want to use. + * Calculate a MeasureSpec value for measuring a child view in one dimension. + * + * @param parentSize Size of the parent view where the child will be placed + * @param padding Total space currently consumed by other elements of the parent + * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT. + * Generally obtained from the child view's LayoutParams + * @param canScroll true if the parent RecyclerView can scroll in this dimension + * @return a MeasureSpec value for the child view + * @deprecated use {@link #getChildMeasureSpec(int, int, int, int, boolean)} */ - public static class Properties { - /** - * {@link android.R.attr#orientation} - */ - public int orientation; - /** - * {@link androidx.recyclerview.R.attr#spanCount} - */ - public int spanCount; - /** - * {@link androidx.recyclerview.R.attr#reverseLayout} - */ - public boolean reverseLayout; - /** - * {@link androidx.recyclerview.R.attr#stackFromEnd} - */ - public boolean stackFromEnd; + @Deprecated + public static int getChildMeasureSpec(int parentSize, int padding, int childDimension, + boolean canScroll) { + int size = Math.max(0, parentSize - padding); + int resultSize = 0; + int resultMode = 0; + if (canScroll) { + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + } else { + // MATCH_PARENT can't be applied since we can scroll in this dimension, wrap + // instead using UNSPECIFIED. + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + } + } else { + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.MATCH_PARENT) { + resultSize = size; + // TODO this should be my spec. + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + resultSize = size; + resultMode = MeasureSpec.AT_MOST; + } + } + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } - } - /** - * An ItemDecoration allows the application to add a special drawing and layout offset - * to specific item views from the adapter's data set. This can be useful for drawing dividers - * between items, highlights, visual grouping boundaries and more. - * - *

    All ItemDecorations are drawn in the order they were added, before the item - * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()} - * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView, - * RecyclerView.State)}.

    - */ - public abstract static class ItemDecoration { /** - * Draw any appropriate decorations into the Canvas supplied to the RecyclerView. - * Any content drawn by this method will be drawn before the item views are drawn, - * and will thus appear underneath the views. + * Calculate a MeasureSpec value for measuring a child view in one dimension. * - * @param c Canvas to draw into - * @param parent RecyclerView this ItemDecoration is drawing into - * @param state The current state of RecyclerView + * @param parentSize Size of the parent view where the child will be placed + * @param parentMode The measurement spec mode of the parent + * @param padding Total space currently consumed by other elements of parent + * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT. + * Generally obtained from the child view's LayoutParams + * @param canScroll true if the parent RecyclerView can scroll in this dimension + * @return a MeasureSpec value for the child view */ - public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) { - onDraw(c, parent); + public static int getChildMeasureSpec(int parentSize, int parentMode, int padding, + int childDimension, boolean canScroll) { + int size = Math.max(0, parentSize - padding); + int resultSize = 0; + int resultMode = 0; + if (canScroll) { + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.MATCH_PARENT) { + switch (parentMode) { + case MeasureSpec.AT_MOST: + case MeasureSpec.EXACTLY: + resultSize = size; + resultMode = parentMode; + break; + case MeasureSpec.UNSPECIFIED: + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + break; + } + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + } + } else { + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.MATCH_PARENT) { + resultSize = size; + resultMode = parentMode; + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + resultSize = size; + if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) { + resultMode = MeasureSpec.AT_MOST; + } else { + resultMode = MeasureSpec.UNSPECIFIED; + } + + } + } + //noinspection WrongConstant + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } /** - * @deprecated Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)} + * Returns the measured width of the given child, plus the additional size of + * any insets applied by {@link ItemDecoration ItemDecorations}. + * + * @param child Child view to query + * @return child's measured width plus ItemDecoration insets + * @see View#getMeasuredWidth() */ - @Deprecated - public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) { + public int getDecoratedMeasuredWidth(@NonNull View child) { + final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; + return child.getMeasuredWidth() + insets.left + insets.right; } /** - * Draw any appropriate decorations into the Canvas supplied to the RecyclerView. - * Any content drawn by this method will be drawn after the item views are drawn - * and will thus appear over the views. + * Returns the measured height of the given child, plus the additional size of + * any insets applied by {@link ItemDecoration ItemDecorations}. * - * @param c Canvas to draw into - * @param parent RecyclerView this ItemDecoration is drawing into - * @param state The current state of RecyclerView. + * @param child Child view to query + * @return child's measured height plus ItemDecoration insets + * @see View#getMeasuredHeight() */ - public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, - @NonNull State state) { - onDrawOver(c, parent); + public int getDecoratedMeasuredHeight(@NonNull View child) { + final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; + return child.getMeasuredHeight() + insets.top + insets.bottom; } /** - * @deprecated Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)} + * Lay out the given child view within the RecyclerView using coordinates that + * include any current {@link ItemDecoration ItemDecorations}. + * + *

    LayoutManagers should prefer working in sizes and coordinates that include + * item decoration insets whenever possible. This allows the LayoutManager to effectively + * ignore decoration insets within measurement and layout code. See the following + * methods:

    + *
      + *
    • {@link #layoutDecoratedWithMargins(View, int, int, int, int)}
    • + *
    • {@link #getDecoratedBoundsWithMargins(View, Rect)}
    • + *
    • {@link #measureChild(View, int, int)}
    • + *
    • {@link #measureChildWithMargins(View, int, int)}
    • + *
    • {@link #getDecoratedLeft(View)}
    • + *
    • {@link #getDecoratedTop(View)}
    • + *
    • {@link #getDecoratedRight(View)}
    • + *
    • {@link #getDecoratedBottom(View)}
    • + *
    • {@link #getDecoratedMeasuredWidth(View)}
    • + *
    • {@link #getDecoratedMeasuredHeight(View)}
    • + *
    + * + * @param child Child to lay out + * @param left Left edge, with item decoration insets included + * @param top Top edge, with item decoration insets included + * @param right Right edge, with item decoration insets included + * @param bottom Bottom edge, with item decoration insets included + * @see View#layout(int, int, int, int) + * @see #layoutDecoratedWithMargins(View, int, int, int, int) */ - @Deprecated - public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) { + public void layoutDecorated(@NonNull View child, int left, int top, int right, int bottom) { + final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; + child.layout(left + insets.left, top + insets.top, right - insets.right, + bottom - insets.bottom); } - /** - * @deprecated Use {@link #getItemOffsets(Rect, View, RecyclerView, State)} + * Lay out the given child view within the RecyclerView using coordinates that + * include any current {@link ItemDecoration ItemDecorations} and margins. + * + *

    LayoutManagers should prefer working in sizes and coordinates that include + * item decoration insets whenever possible. This allows the LayoutManager to effectively + * ignore decoration insets within measurement and layout code. See the following + * methods:

    + *
      + *
    • {@link #layoutDecorated(View, int, int, int, int)}
    • + *
    • {@link #measureChild(View, int, int)}
    • + *
    • {@link #measureChildWithMargins(View, int, int)}
    • + *
    • {@link #getDecoratedLeft(View)}
    • + *
    • {@link #getDecoratedTop(View)}
    • + *
    • {@link #getDecoratedRight(View)}
    • + *
    • {@link #getDecoratedBottom(View)}
    • + *
    • {@link #getDecoratedMeasuredWidth(View)}
    • + *
    • {@link #getDecoratedMeasuredHeight(View)}
    • + *
    + * + * @param child Child to lay out + * @param left Left edge, with item decoration insets and left margin included + * @param top Top edge, with item decoration insets and top margin included + * @param right Right edge, with item decoration insets and right margin included + * @param bottom Bottom edge, with item decoration insets and bottom margin included + * @see View#layout(int, int, int, int) + * @see #layoutDecorated(View, int, int, int, int) */ - @Deprecated - public void getItemOffsets(@NonNull Rect outRect, int itemPosition, - @NonNull RecyclerView parent) { - outRect.set(0, 0, 0, 0); + public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right, + int bottom) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + final Rect insets = lp.mDecorInsets; + child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin, + right - insets.right - lp.rightMargin, + bottom - insets.bottom - lp.bottomMargin); } /** - * Retrieve any offsets for the given item. Each field of outRect specifies - * the number of pixels that the item view should be inset by, similar to padding or margin. - * The default implementation sets the bounds of outRect to 0 and returns. - * - *

    - * If this ItemDecoration does not affect the positioning of item views, it should set - * all four fields of outRect (left, top, right, bottom) to zero - * before returning. - * + * Calculates the bounding box of the View while taking into account its matrix changes + * (translation, scale etc) with respect to the RecyclerView. *

    - * If you need to access Adapter for additional data, you can call - * {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the - * View. + * If {@code includeDecorInsets} is {@code true}, they are applied first before applying + * the View's matrix so that the decor offsets also go through the same transformation. * - * @param outRect Rect to receive the output. - * @param view The child view to decorate - * @param parent RecyclerView this ItemDecoration is decorating - * @param state The current state of RecyclerView. + * @param child The ItemView whose bounding box should be calculated. + * @param includeDecorInsets True if the decor insets should be included in the bounding box + * @param out The rectangle into which the output will be written. */ - public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, - @NonNull RecyclerView parent, @NonNull State state) { - getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(), - parent); - } - } - - /** - * An implementation of {@link RecyclerView.OnItemTouchListener} that has empty method bodies - * and default return values. - *

    - * You may prefer to extend this class if you don't need to override all methods. Another - * benefit of using this class is future compatibility. As the interface may change, we'll - * always provide a default implementation on this class so that your code won't break when - * you update to a new version of the support library. - */ - public static class SimpleOnItemTouchListener implements RecyclerView.OnItemTouchListener { - @Override - public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { - return false; - } - - @Override - public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { - } + public void getTransformedBoundingBox(@NonNull View child, boolean includeDecorInsets, + @NonNull Rect out) { + if (includeDecorInsets) { + Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; + out.set(-insets.left, -insets.top, + child.getWidth() + insets.right, child.getHeight() + insets.bottom); + } else { + out.set(0, 0, child.getWidth(), child.getHeight()); + } - @Override - public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (mRecyclerView != null) { + final Matrix childMatrix = child.getMatrix(); + if (childMatrix != null && !childMatrix.isIdentity()) { + final RectF tempRectF = mRecyclerView.mTempRectF; + tempRectF.set(out); + childMatrix.mapRect(tempRectF); + out.set( + (int) Math.floor(tempRectF.left), + (int) Math.floor(tempRectF.top), + (int) Math.ceil(tempRectF.right), + (int) Math.ceil(tempRectF.bottom) + ); + } + } + out.offset(child.getLeft(), child.getTop()); } - } - /** - * An OnScrollListener can be added to a RecyclerView to receive messages when a scrolling event - * has occurred on that RecyclerView. - *

    - * - * @see RecyclerView#addOnScrollListener(OnScrollListener) - * @see RecyclerView#clearOnChildAttachStateChangeListeners() - */ - public abstract static class OnScrollListener { /** - * Callback method to be invoked when RecyclerView's scroll state changes. + * Returns the bounds of the view including its decoration and margins. * - * @param recyclerView The RecyclerView whose scroll state has changed. - * @param newState The updated scroll state. One of {@link #SCROLL_STATE_IDLE}, - * {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}. + * @param view The view element to check + * @param outBounds A rect that will receive the bounds of the element including its + * decoration and margins. */ - public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + public void getDecoratedBoundsWithMargins(@NonNull View view, @NonNull Rect outBounds) { + RecyclerView.getDecoratedBoundsWithMarginsInt(view, outBounds); } /** - * Callback method to be invoked when the RecyclerView has been scrolled. This will be - * called after the scroll has completed. - *

    - * This callback will also be called if visible item range changes after a layout - * calculation. In that case, dx and dy will be 0. + * Returns the left edge of the given child view within its parent, offset by any applied + * {@link ItemDecoration ItemDecorations}. * - * @param recyclerView The RecyclerView which scrolled. - * @param dx The amount of horizontal scroll. - * @param dy The amount of vertical scroll. + * @param child Child to query + * @return Child left edge with offsets applied + * @see #getLeftDecorationWidth(View) */ - public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + public int getDecoratedLeft(@NonNull View child) { + return child.getLeft() - getLeftDecorationWidth(child); } - } - /** - * A ViewHolder describes an item view and metadata about its place within the RecyclerView. - * - *

    {@link Adapter} implementations should subclass ViewHolder and add fields for caching - * potentially expensive {@link View#findViewById(int)} results.

    - * - *

    While {@link LayoutParams} belong to the {@link LayoutManager}, - * {@link ViewHolder ViewHolders} belong to the adapter. Adapters should feel free to use - * their own custom ViewHolder implementations to store data that makes binding view contents - * easier. Implementations should assume that individual item views will hold strong references - * to ViewHolder objects and that RecyclerView instances may hold - * strong references to extra off-screen item views for caching purposes

    - */ - public abstract static class ViewHolder { - /** - * This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType - * are all valid. - */ - static final int FLAG_BOUND = 1; - /** - * The data this ViewHolder's view reflects is stale and needs to be rebound - * by the adapter. mPosition and mItemId are consistent. - */ - static final int FLAG_UPDATE = 1 << 1; - /** - * This ViewHolder's data is invalid. The identity implied by mPosition and mItemId - * are not to be trusted and may no longer match the item view type. - * This ViewHolder must be fully rebound to different data. - */ - static final int FLAG_INVALID = 1 << 2; - /** - * This ViewHolder points at data that represents an item previously removed from the - * data set. Its view may still be used for things like outgoing animations. - */ - static final int FLAG_REMOVED = 1 << 3; - /** - * This ViewHolder should not be recycled. This flag is set via setIsRecyclable() - * and is intended to keep views around during animations. - */ - static final int FLAG_NOT_RECYCLABLE = 1 << 4; - /** - * This ViewHolder is returned from scrap which means we are expecting an addView call - * for this itemView. When returned from scrap, ViewHolder stays in the scrap list until - * the end of the layout pass and then recycled by RecyclerView if it is not added back to - * the RecyclerView. - */ - static final int FLAG_RETURNED_FROM_SCRAP = 1 << 5; /** - * This ViewHolder is fully managed by the LayoutManager. We do not scrap, recycle or remove - * it unless LayoutManager is replaced. - * It is still fully visible to the LayoutManager. + * Returns the top edge of the given child view within its parent, offset by any applied + * {@link ItemDecoration ItemDecorations}. + * + * @param child Child to query + * @return Child top edge with offsets applied + * @see #getTopDecorationHeight(View) */ - static final int FLAG_IGNORE = 1 << 7; + public int getDecoratedTop(@NonNull View child) { + return child.getTop() - getTopDecorationHeight(child); + } + /** - * When the View is detached form the parent, we set this flag so that we can take correct - * action when we need to remove it or add it back. + * Returns the right edge of the given child view within its parent, offset by any applied + * {@link ItemDecoration ItemDecorations}. + * + * @param child Child to query + * @return Child right edge with offsets applied + * @see #getRightDecorationWidth(View) */ - static final int FLAG_TMP_DETACHED = 1 << 8; + public int getDecoratedRight(@NonNull View child) { + return child.getRight() + getRightDecorationWidth(child); + } + /** - * Set when we can no longer determine the adapter position of this ViewHolder until it is - * rebound to a new position. It is different than FLAG_INVALID because FLAG_INVALID is - * set even when the type does not match. Also, FLAG_ADAPTER_POSITION_UNKNOWN is set as soon - * as adapter notification arrives vs FLAG_INVALID is set lazily before layout is - * re-calculated. + * Returns the bottom edge of the given child view within its parent, offset by any applied + * {@link ItemDecoration ItemDecorations}. + * + * @param child Child to query + * @return Child bottom edge with offsets applied + * @see #getBottomDecorationHeight(View) */ - static final int FLAG_ADAPTER_POSITION_UNKNOWN = 1 << 9; + public int getDecoratedBottom(@NonNull View child) { + return child.getBottom() + getBottomDecorationHeight(child); + } + /** - * Set when a addChangePayload(null) is called + * Calculates the item decor insets applied to the given child and updates the provided + * Rect instance with the inset values. + *
      + *
    • The Rect's left is set to the total width of left decorations.
    • + *
    • The Rect's top is set to the total height of top decorations.
    • + *
    • The Rect's right is set to the total width of right decorations.
    • + *
    • The Rect's bottom is set to total height of bottom decorations.
    • + *
    + *

    + * Note that item decorations are automatically calculated when one of the LayoutManager's + * measure child methods is called. If you need to measure the child with custom specs via + * {@link View#measure(int, int)}, you can use this method to get decorations. + * + * @param child The child view whose decorations should be calculated + * @param outRect The Rect to hold result values */ - static final int FLAG_ADAPTER_FULLUPDATE = 1 << 10; + public void calculateItemDecorationsForChild(@NonNull View child, @NonNull Rect outRect) { + if (mRecyclerView == null) { + outRect.set(0, 0, 0, 0); + return; + } + Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); + outRect.set(insets); + } + /** - * Used by ItemAnimator when a ViewHolder's position changes + * Returns the total height of item decorations applied to child's top. + *

    + * Note that this value is not updated until the View is measured or + * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * + * @param child Child to query + * @return The total height of item decorations applied to the child's top. + * @see #getDecoratedTop(View) + * @see #calculateItemDecorationsForChild(View, Rect) */ - static final int FLAG_MOVED = 1 << 11; + public int getTopDecorationHeight(@NonNull View child) { + return ((LayoutParams) child.getLayoutParams()).mDecorInsets.top; + } + /** - * Used by ItemAnimator when a ViewHolder appears in pre-layout + * Returns the total height of item decorations applied to child's bottom. + *

    + * Note that this value is not updated until the View is measured or + * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * + * @param child Child to query + * @return The total height of item decorations applied to the child's bottom. + * @see #getDecoratedBottom(View) + * @see #calculateItemDecorationsForChild(View, Rect) */ - static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12; - static final int PENDING_ACCESSIBILITY_STATE_NOT_SET = -1; + public int getBottomDecorationHeight(@NonNull View child) { + return ((LayoutParams) child.getLayoutParams()).mDecorInsets.bottom; + } + /** - * Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from - * hidden list (as if it was scrap) without being recycled in between. - *

    - * When a ViewHolder is hidden, there are 2 paths it can be re-used: - * a) Animation ends, view is recycled and used from the recycle pool. - * b) LayoutManager asks for the View for that position while the ViewHolder is hidden. + * Returns the total width of item decorations applied to child's left. *

    - * This flag is used to represent "case b" where the ViewHolder is reused without being - * recycled (thus "bounced" from the hidden list). This state requires special handling - * because the ViewHolder must be added to pre layout maps for animations as if it was - * already there. + * Note that this value is not updated until the View is measured or + * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * + * @param child Child to query + * @return The total width of item decorations applied to the child's left. + * @see #getDecoratedLeft(View) + * @see #calculateItemDecorationsForChild(View, Rect) */ - static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13; - private static final List FULLUPDATE_PAYLOADS = Collections.emptyList(); - @NonNull - public final View itemView; - WeakReference mNestedRecyclerView; - int mPosition = NO_POSITION; - int mOldPosition = NO_POSITION; - long mItemId = NO_ID; - int mItemViewType = INVALID_TYPE; - int mPreLayoutPosition = NO_POSITION; - // The item that this holder is shadowing during an item change event/animation - ViewHolder mShadowedHolder; - // The item that is shadowing this holder during an item change event/animation - ViewHolder mShadowingHolder; - int mFlags; - List mPayloads; - List mUnmodifiedPayloads; - // If non-null, view is currently considered scrap and may be reused for other data by the - // scrap container. - Recycler mScrapContainer; - // Keeps whether this ViewHolder lives in Change scrap or Attached scrap - boolean mInChangeScrap; - // set if we defer the accessibility state change of the view holder - @VisibleForTesting - int mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET; + public int getLeftDecorationWidth(@NonNull View child) { + return ((LayoutParams) child.getLayoutParams()).mDecorInsets.left; + } + /** - * Is set when VH is bound from the adapter and cleaned right before it is sent to - * {@link RecycledViewPool}. + * Returns the total width of item decorations applied to child's right. + *

    + * Note that this value is not updated until the View is measured or + * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * + * @param child Child to query + * @return The total width of item decorations applied to the child's right. + * @see #getDecoratedRight(View) + * @see #calculateItemDecorationsForChild(View, Rect) */ - RecyclerView mOwnerRecyclerView; - // The last adapter that bound this ViewHolder. It is cleaned before VH is recycled. - Adapter mBindingAdapter; - private int mIsRecyclableCount; - // Saves isImportantForAccessibility value for the view item while it's in hidden state and - // marked as unimportant for accessibility. - private int mWasImportantForAccessibilityBeforeHidden = - ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; - - public ViewHolder(@NonNull View itemView) { - if (itemView == null) { - throw new IllegalArgumentException("itemView may not be null"); - } - this.itemView = itemView; + public int getRightDecorationWidth(@NonNull View child) { + return ((LayoutParams) child.getLayoutParams()).mDecorInsets.right; } - void flagRemovedAndOffsetPosition(int mNewPosition, int offset, boolean applyToPreLayout) { - addFlags(FLAG_REMOVED); - offsetPosition(offset, applyToPreLayout); - mPosition = mNewPosition; + /** + * Called when searching for a focusable view in the given direction has failed + * for the current content of the RecyclerView. + * + *

    This is the LayoutManager's opportunity to populate views in the given direction + * to fulfill the request if it can. The LayoutManager should attach and return + * the view to be focused, if a focusable view in the given direction is found. + * Otherwise, if all the existing (or the newly populated views) are unfocusable, it returns + * the next unfocusable view to become visible on the screen. This unfocusable view is + * typically the first view that's either partially or fully out of RV's padded bounded + * area in the given direction. The default implementation returns null.

    + * + * @param focused The currently focused view + * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * or 0 for not applicable + * @param recycler The recycler to use for obtaining views for currently offscreen items + * @param state Transient state of RecyclerView + * @return The chosen view to be focused if a focusable view is found, otherwise an + * unfocusable view to become visible onto the screen, else null. + */ + @Nullable + public View onFocusSearchFailed(@NonNull View focused, int direction, + @NonNull Recycler recycler, @NonNull State state) { + return null; } - void offsetPosition(int offset, boolean applyToPreLayout) { - if (mOldPosition == NO_POSITION) { - mOldPosition = mPosition; - } - if (mPreLayoutPosition == NO_POSITION) { - mPreLayoutPosition = mPosition; - } - if (applyToPreLayout) { - mPreLayoutPosition += offset; - } - mPosition += offset; - if (itemView.getLayoutParams() != null) { - ((LayoutParams) itemView.getLayoutParams()).mInsetsDirty = true; - } + /** + * This method gives a LayoutManager an opportunity to intercept the initial focus search + * before the default behavior of {@link FocusFinder} is used. If this method returns + * null FocusFinder will attempt to find a focusable child view. If it fails + * then {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)} + * will be called to give the LayoutManager an opportunity to add new views for items + * that did not have attached views representing them. The LayoutManager should not add + * or remove views from this method. + * + * @param focused The currently focused view + * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * @return A descendant view to focus or null to fall back to default behavior. + * The default implementation returns null. + */ + @Nullable + public View onInterceptFocusSearch(@NonNull View focused, int direction) { + return null; } - void clearOldPosition() { - mOldPosition = NO_POSITION; - mPreLayoutPosition = NO_POSITION; - } + /** + * Returns the scroll amount that brings the given rect in child's coordinate system within + * the padded area of RecyclerView. + * + * @param child The direct child making the request. + * @param rect The rectangle in the child's coordinates the child + * wishes to be on the screen. + * @return The array containing the scroll amount in x and y directions that brings the + * given rect into RV's padded area. + */ + private int[] getChildRectangleOnScreenScrollAmount(View child, Rect rect) { + int[] out = new int[2]; + final int parentLeft = getPaddingLeft(); + final int parentTop = getPaddingTop(); + final int parentRight = getWidth() - getPaddingRight(); + final int parentBottom = getHeight() - getPaddingBottom(); + final int childLeft = child.getLeft() + rect.left - child.getScrollX(); + final int childTop = child.getTop() + rect.top - child.getScrollY(); + final int childRight = childLeft + rect.width(); + final int childBottom = childTop + rect.height(); + + final int offScreenLeft = Math.min(0, childLeft - parentLeft); + final int offScreenTop = Math.min(0, childTop - parentTop); + final int offScreenRight = Math.max(0, childRight - parentRight); + final int offScreenBottom = Math.max(0, childBottom - parentBottom); - void saveOldPosition() { - if (mOldPosition == NO_POSITION) { - mOldPosition = mPosition; + // Favor the "start" layout direction over the end when bringing one side or the other + // of a large rect into view. If we decide to bring in end because start is already + // visible, limit the scroll such that start won't go out of bounds. + final int dx; + if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) { + dx = offScreenRight != 0 ? offScreenRight + : Math.max(offScreenLeft, childRight - parentRight); + } else { + dx = offScreenLeft != 0 ? offScreenLeft + : Math.min(childLeft - parentLeft, offScreenRight); } - } - boolean shouldIgnore() { - return (mFlags & FLAG_IGNORE) != 0; + // Favor bringing the top into view over the bottom. If top is already visible and + // we should scroll to make bottom visible, make sure top does not go out of bounds. + final int dy = offScreenTop != 0 ? offScreenTop + : Math.min(childTop - parentTop, offScreenBottom); + out[0] = dx; + out[1] = dy; + return out; } /** - * @see #getLayoutPosition() - * @see #getBindingAdapterPosition() - * @see #getAbsoluteAdapterPosition() - * @deprecated This method is deprecated because its meaning is ambiguous due to the async - * handling of adapter updates. You should use {@link #getLayoutPosition()}, - * {@link #getBindingAdapterPosition()} or {@link #getAbsoluteAdapterPosition()} - * depending on your use case. + * Called when a child of the RecyclerView wants a particular rectangle to be positioned + * onto the screen. See {@link ViewParent#requestChildRectangleOnScreen(android.view.View, + * android.graphics.Rect, boolean)} for more details. + * + *

    The base implementation will attempt to perform a standard programmatic scroll + * to bring the given rect into view, within the padded area of the RecyclerView.

    + * + * @param child The direct child making the request. + * @param rect The rectangle in the child's coordinates the child + * wishes to be on the screen. + * @param immediate True to forbid animated or delayed scrolling, + * false otherwise + * @return Whether the group scrolled to handle the operation */ - @Deprecated - public final int getPosition() { - return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition; + public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, + @NonNull View child, @NonNull Rect rect, boolean immediate) { + return requestChildRectangleOnScreen(parent, child, rect, immediate, false); } /** - * Returns the position of the ViewHolder in terms of the latest layout pass. - *

    - * This position is mostly used by RecyclerView components to be consistent while - * RecyclerView lazily processes adapter updates. - *

    - * For performance and animation reasons, RecyclerView batches all adapter updates until the - * next layout pass. This may cause mismatches between the Adapter position of the item and - * the position it had in the latest layout calculations. - *

    - * LayoutManagers should always call this method while doing calculations based on item - * positions. All methods in {@link RecyclerView.LayoutManager}, {@link RecyclerView.State}, - * {@link RecyclerView.Recycler} that receive a position expect it to be the layout position - * of the item. - *

    - * If LayoutManager needs to call an external method that requires the adapter position of - * the item, it can use {@link #getAbsoluteAdapterPosition()} or - * {@link RecyclerView.Recycler#convertPreLayoutPositionToPostLayout(int)}. + * Requests that the given child of the RecyclerView be positioned onto the screen. This + * method can be called for both unfocusable and focusable child views. For unfocusable + * child views, focusedChildVisible is typically true in which case, layout manager + * makes the child view visible only if the currently focused child stays in-bounds of RV. * - * @return Returns the adapter position of the ViewHolder in the latest layout pass. - * @see #getBindingAdapterPosition() - * @see #getAbsoluteAdapterPosition() + * @param parent The parent RecyclerView. + * @param child The direct child making the request. + * @param rect The rectangle in the child's coordinates the child + * wishes to be on the screen. + * @param immediate True to forbid animated or delayed scrolling, + * false otherwise + * @param focusedChildVisible Whether the currently focused view must stay visible. + * @return Whether the group scrolled to handle the operation */ - public final int getLayoutPosition() { - return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition; + public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, + @NonNull View child, @NonNull Rect rect, boolean immediate, + boolean focusedChildVisible) { + int[] scrollAmount = getChildRectangleOnScreenScrollAmount(child, rect + ); + int dx = scrollAmount[0]; + int dy = scrollAmount[1]; + if (!focusedChildVisible || isFocusedChildVisibleAfterScrolling(parent, dx, dy)) { + if (dx != 0 || dy != 0) { + if (immediate) { + parent.scrollBy(dx, dy); + } else { + parent.smoothScrollBy(dx, dy); + } + return true; + } + } + return false; } - /** - * @return {@link #getBindingAdapterPosition()} - * @deprecated This method is confusing when adapters nest other adapters. - * If you are calling this in the context of an Adapter, you probably want to call - * {@link #getBindingAdapterPosition()} or if you want the position as {@link RecyclerView} - * sees it, you should call {@link #getAbsoluteAdapterPosition()}. + * Returns whether the given child view is partially or fully visible within the padded + * bounded area of RecyclerView, depending on the input parameters. + * A view is partially visible if it has non-zero overlap with RV's padded bounded area. + * If acceptEndPointInclusion flag is set to true, it's also considered partially + * visible if it's located outside RV's bounds and it's hitting either RV's start or end + * bounds. + * + * @param child The child view to be examined. + * @param completelyVisible If true, the method returns true if and only if the + * child is + * completely visible. If false, the method returns true + * if and + * only if the child is only partially visible (that is it + * will + * return false if the child is either completely visible + * or out + * of RV's bounds). + * @param acceptEndPointInclusion If the view's endpoint intersection with RV's start of end + * bounds is enough to consider it partially visible, + * false otherwise. + * @return True if the given child is partially or fully visible, false otherwise. */ - @Deprecated - public final int getAdapterPosition() { - return getBindingAdapterPosition(); + public boolean isViewPartiallyVisible(@NonNull View child, boolean completelyVisible, + boolean acceptEndPointInclusion) { + int boundsFlag = (ViewBoundsCheck.FLAG_CVS_GT_PVS | ViewBoundsCheck.FLAG_CVS_EQ_PVS + | ViewBoundsCheck.FLAG_CVE_LT_PVE | ViewBoundsCheck.FLAG_CVE_EQ_PVE); + boolean isViewFullyVisible = mHorizontalBoundCheck.isViewWithinBoundFlags(child, + boundsFlag) + && mVerticalBoundCheck.isViewWithinBoundFlags(child, boundsFlag); + if (completelyVisible) { + return isViewFullyVisible; + } else { + return !isViewFullyVisible; + } } /** - * Returns the Adapter position of the item represented by this ViewHolder with respect to - * the {@link Adapter} that bound it. - *

    - * Note that this might be different than the {@link #getLayoutPosition()} if there are - * pending adapter updates but a new layout pass has not happened yet. - *

    - * RecyclerView does not handle any adapter updates until the next layout traversal. This - * may create temporary inconsistencies between what user sees on the screen and what - * adapter contents have. This inconsistency is not important since it will be less than - * 16ms but it might be a problem if you want to use ViewHolder position to access the - * adapter. Sometimes, you may need to get the exact adapter position to do - * some actions in response to user events. In that case, you should use this method which - * will calculate the Adapter position of the ViewHolder. - *

    - * Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the - * next layout pass, the return value of this method will be {@link #NO_POSITION}. - *

    - * If the {@link Adapter} that bound this {@link ViewHolder} is inside another - * {@link Adapter} (e.g. {@link ConcatAdapter}), this position might be different than - * {@link #getAbsoluteAdapterPosition()}. If you would like to know the position that - * {@link RecyclerView} considers (e.g. for saved state), you should use - * {@link #getAbsoluteAdapterPosition()}. + * Returns whether the currently focused child stays within RV's bounds with the given + * amount of scrolling. * - * @return The adapter position of the item if it still exists in the adapter. - * {@link RecyclerView#NO_POSITION} if item has been removed from the adapter, - * {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last - * layout pass or the ViewHolder has already been recycled. - * @see #getAbsoluteAdapterPosition() - * @see #getLayoutPosition() + * @param parent The parent RecyclerView. + * @param dx The scrolling in x-axis direction to be performed. + * @param dy The scrolling in y-axis direction to be performed. + * @return {@code false} if the focused child is not at least partially visible after + * scrolling or no focused child exists, {@code true} otherwise. */ - public final int getBindingAdapterPosition() { - if (mBindingAdapter == null) { - return NO_POSITION; - } - if (mOwnerRecyclerView == null) { - return NO_POSITION; - } - @SuppressWarnings("unchecked") - Adapter rvAdapter = mOwnerRecyclerView.getAdapter(); - if (rvAdapter == null) { - return NO_POSITION; + private boolean isFocusedChildVisibleAfterScrolling(RecyclerView parent, int dx, int dy) { + final View focusedChild = parent.getFocusedChild(); + if (focusedChild == null) { + return false; } - int globalPosition = mOwnerRecyclerView.getAdapterPositionInRecyclerView(this); - if (globalPosition == NO_POSITION) { - return NO_POSITION; + final int parentLeft = getPaddingLeft(); + final int parentTop = getPaddingTop(); + final int parentRight = getWidth() - getPaddingRight(); + final int parentBottom = getHeight() - getPaddingBottom(); + final Rect bounds = mRecyclerView.mTempRect; + getDecoratedBoundsWithMargins(focusedChild, bounds); + + if (bounds.left - dx >= parentRight || bounds.right - dx <= parentLeft + || bounds.top - dy >= parentBottom || bounds.bottom - dy <= parentTop) { + return false; } - return rvAdapter.findRelativeAdapterPositionIn(mBindingAdapter, this, globalPosition); + return true; + } + + /** + * @deprecated Use {@link #onRequestChildFocus(RecyclerView, State, View, View)} + */ + @Deprecated + public boolean onRequestChildFocus(@NonNull RecyclerView parent, @NonNull View child, + @Nullable View focused) { + // eat the request if we are in the middle of a scroll or layout + return isSmoothScrolling() || parent.isComputingLayout(); } /** - * Returns the Adapter position of the item represented by this ViewHolder with respect to - * the {@link RecyclerView}'s {@link Adapter}. If the {@link Adapter} that bound this - * {@link ViewHolder} is inside another adapter (e.g. {@link ConcatAdapter}), this - * position might be different and will include - * the offsets caused by other adapters in the {@link ConcatAdapter}. - *

    - * Note that this might be different than the {@link #getLayoutPosition()} if there are - * pending adapter updates but a new layout pass has not happened yet. - *

    - * RecyclerView does not handle any adapter updates until the next layout traversal. This - * may create temporary inconsistencies between what user sees on the screen and what - * adapter contents have. This inconsistency is not important since it will be less than - * 16ms but it might be a problem if you want to use ViewHolder position to access the - * adapter. Sometimes, you may need to get the exact adapter position to do - * some actions in response to user events. In that case, you should use this method which - * will calculate the Adapter position of the ViewHolder. - *

    - * Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the - * next layout pass, the return value of this method will be {@link #NO_POSITION}. - *

    - * Note that if you are querying the position as {@link RecyclerView} sees, you should use - * {@link #getAbsoluteAdapterPosition()} (e.g. you want to use it to save scroll - * state). If you are querying the position to access the {@link Adapter} contents, - * you should use {@link #getBindingAdapterPosition()}. + * Called when a descendant view of the RecyclerView requests focus. * - * @return The adapter position of the item from {@link RecyclerView}'s perspective if it - * still exists in the adapter and bound to a valid item. - * {@link RecyclerView#NO_POSITION} if item has been removed from the adapter, - * {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last - * layout pass or the ViewHolder has already been recycled. - * @see #getBindingAdapterPosition() - * @see #getLayoutPosition() + *

    A LayoutManager wishing to keep focused views aligned in a specific + * portion of the view may implement that behavior in an override of this method.

    + * + *

    If the LayoutManager executes different behavior that should override the default + * behavior of scrolling the focused child on screen instead of running alongside it, + * this method should return true.

    + * + * @param parent The RecyclerView hosting this LayoutManager + * @param state Current state of RecyclerView + * @param child Direct child of the RecyclerView containing the newly focused view + * @param focused The newly focused view. This may be the same view as child or it may be + * null + * @return true if the default scroll behavior should be suppressed */ - public final int getAbsoluteAdapterPosition() { - if (mOwnerRecyclerView == null) { - return NO_POSITION; - } - return mOwnerRecyclerView.getAdapterPositionInRecyclerView(this); + public boolean onRequestChildFocus(@NonNull RecyclerView parent, @NonNull State state, + @NonNull View child, @Nullable View focused) { + return onRequestChildFocus(parent, child, focused); } /** - * Returns the {@link Adapter} that last bound this {@link ViewHolder}. - * Might return {@code null} if this {@link ViewHolder} is not bound to any adapter. + * Called if the RecyclerView this LayoutManager is bound to has a different adapter set via + * {@link RecyclerView#setAdapter(Adapter)} or + * {@link RecyclerView#swapAdapter(Adapter, boolean)}. The LayoutManager may use this + * opportunity to clear caches and configure state such that it can relayout appropriately + * with the new data and potentially new view types. * - * @return The {@link Adapter} that last bound this {@link ViewHolder} or {@code null} if - * this {@link ViewHolder} is not bound by any adapter (e.g. recycled). + *

    The default implementation removes all currently attached views.

    + * + * @param oldAdapter The previous adapter instance. Will be null if there was previously no + * adapter. + * @param newAdapter The new adapter instance. Might be null if + * {@link RecyclerView#setAdapter(RecyclerView.Adapter)} is called with + * {@code null}. */ - @Nullable - public final Adapter getBindingAdapter() { - return mBindingAdapter; + public void onAdapterChanged(@Nullable Adapter oldAdapter, @Nullable Adapter newAdapter) { } /** - * When LayoutManager supports animations, RecyclerView tracks 3 positions for ViewHolders - * to perform animations. - *

    - * If a ViewHolder was laid out in the previous onLayout call, old position will keep its - * adapter index in the previous layout. + * Called to populate focusable views within the RecyclerView. * - * @return The previous adapter index of the Item represented by this ViewHolder or - * {@link #NO_POSITION} if old position does not exists or cleared (pre-layout is - * complete). + *

    The LayoutManager implementation should return true if the default + * behavior of {@link ViewGroup#addFocusables(java.util.ArrayList, int)} should be + * suppressed.

    + * + *

    The default implementation returns false to trigger RecyclerView + * to fall back to the default ViewGroup behavior.

    + * + * @param recyclerView The RecyclerView hosting this LayoutManager + * @param views List of output views. This method should add valid focusable views + * to this list. + * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * @param focusableMode The type of focusables to be added. + * @return true to suppress the default behavior, false to add default focusables after + * this method returns. + * @see #FOCUSABLES_ALL + * @see #FOCUSABLES_TOUCH_MODE */ - public final int getOldPosition() { - return mOldPosition; + public boolean onAddFocusables(@NonNull RecyclerView recyclerView, + @NonNull ArrayList views, int direction, int focusableMode) { + return false; } /** - * Returns The itemId represented by this ViewHolder. - * - * @return The item's id if adapter has stable ids, {@link RecyclerView#NO_ID} - * otherwise + * Called in response to a call to {@link Adapter#notifyDataSetChanged()} or + * {@link RecyclerView#swapAdapter(Adapter, boolean)} ()} and signals that the the entire + * data set has changed. */ - public final long getItemId() { - return mItemId; + public void onItemsChanged(@NonNull RecyclerView recyclerView) { } /** - * @return The view type of this ViewHolder. + * Called when items have been added to the adapter. The LayoutManager may choose to + * requestLayout if the inserted items would require refreshing the currently visible set + * of child views. (e.g. currently empty space would be filled by appended items, etc.) */ - public final int getItemViewType() { - return mItemViewType; + public void onItemsAdded(@NonNull RecyclerView recyclerView, int positionStart, + int itemCount) { } - boolean isScrap() { - return mScrapContainer != null; + /** + * Called when items have been removed from the adapter. + */ + public void onItemsRemoved(@NonNull RecyclerView recyclerView, int positionStart, + int itemCount) { } - void unScrap() { - mScrapContainer.unscrapView(this); + /** + * Called when items have been changed in the adapter. + * To receive payload, override {@link #onItemsUpdated(RecyclerView, int, int, Object)} + * instead, then this callback will not be invoked. + */ + public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart, + int itemCount) { } - boolean wasReturnedFromScrap() { - return (mFlags & FLAG_RETURNED_FROM_SCRAP) != 0; + /** + * Called when items have been changed in the adapter and with optional payload. + * Default implementation calls {@link #onItemsUpdated(RecyclerView, int, int)}. + */ + public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart, + int itemCount, @Nullable Object payload) { + onItemsUpdated(recyclerView, positionStart, itemCount); } - void clearReturnedFromScrapFlag() { - mFlags = mFlags & ~FLAG_RETURNED_FROM_SCRAP; + /** + * Called when an item is moved withing the adapter. + *

    + * Note that, an item may also change position in response to another ADD/REMOVE/MOVE + * operation. This callback is only called if and only if {@link Adapter#notifyItemMoved} + * is called. + */ + public void onItemsMoved(@NonNull RecyclerView recyclerView, int from, int to, + int itemCount) { + } - void clearTmpDetachFlag() { - mFlags = mFlags & ~FLAG_TMP_DETACHED; + + /** + *

    Override this method if you want to support scroll bars.

    + * + *

    Read {@link RecyclerView#computeHorizontalScrollExtent()} for details.

    + * + *

    Default implementation returns 0.

    + * + * @param state Current state of RecyclerView + * @return The horizontal extent of the scrollbar's thumb + * @see RecyclerView#computeHorizontalScrollExtent() + */ + public int computeHorizontalScrollExtent(@NonNull State state) { + return 0; } - void stopIgnoring() { - mFlags = mFlags & ~FLAG_IGNORE; + /** + *

    Override this method if you want to support scroll bars.

    + * + *

    Read {@link RecyclerView#computeHorizontalScrollOffset()} for details.

    + * + *

    Default implementation returns 0.

    + * + * @param state Current State of RecyclerView where you can find total item count + * @return The horizontal offset of the scrollbar's thumb + * @see RecyclerView#computeHorizontalScrollOffset() + */ + public int computeHorizontalScrollOffset(@NonNull State state) { + return 0; } - void setScrapContainer(Recycler recycler, boolean isChangeScrap) { - mScrapContainer = recycler; - mInChangeScrap = isChangeScrap; + /** + *

    Override this method if you want to support scroll bars.

    + * + *

    Read {@link RecyclerView#computeHorizontalScrollRange()} for details.

    + * + *

    Default implementation returns 0.

    + * + * @param state Current State of RecyclerView where you can find total item count + * @return The total horizontal range represented by the horizontal scrollbar + * @see RecyclerView#computeHorizontalScrollRange() + */ + public int computeHorizontalScrollRange(@NonNull State state) { + return 0; } - boolean isInvalid() { - return (mFlags & FLAG_INVALID) != 0; + /** + *

    Override this method if you want to support scroll bars.

    + * + *

    Read {@link RecyclerView#computeVerticalScrollExtent()} for details.

    + * + *

    Default implementation returns 0.

    + * + * @param state Current state of RecyclerView + * @return The vertical extent of the scrollbar's thumb + * @see RecyclerView#computeVerticalScrollExtent() + */ + public int computeVerticalScrollExtent(@NonNull State state) { + return 0; } - boolean needsUpdate() { - return (mFlags & FLAG_UPDATE) != 0; + /** + *

    Override this method if you want to support scroll bars.

    + * + *

    Read {@link RecyclerView#computeVerticalScrollOffset()} for details.

    + * + *

    Default implementation returns 0.

    + * + * @param state Current State of RecyclerView where you can find total item count + * @return The vertical offset of the scrollbar's thumb + * @see RecyclerView#computeVerticalScrollOffset() + */ + public int computeVerticalScrollOffset(@NonNull State state) { + return 0; } - boolean isBound() { - return (mFlags & FLAG_BOUND) != 0; + /** + *

    Override this method if you want to support scroll bars.

    + * + *

    Read {@link RecyclerView#computeVerticalScrollRange()} for details.

    + * + *

    Default implementation returns 0.

    + * + * @param state Current State of RecyclerView where you can find total item count + * @return The total vertical range represented by the vertical scrollbar + * @see RecyclerView#computeVerticalScrollRange() + */ + public int computeVerticalScrollRange(@NonNull State state) { + return 0; } - boolean isRemoved() { - return (mFlags & FLAG_REMOVED) != 0; + /** + * Measure the attached RecyclerView. Implementations must call + * {@link #setMeasuredDimension(int, int)} before returning. + *

    + * It is strongly advised to use the AutoMeasure mechanism by overriding + * {@link #isAutoMeasureEnabled()} to return true as AutoMeasure handles all the standard + * measure cases including when the RecyclerView's layout_width or layout_height have been + * set to wrap_content. If {@link #isAutoMeasureEnabled()} is overridden to return true, + * this method should not be overridden. + *

    + * The default implementation will handle EXACTLY measurements and respect + * the minimum width and height properties of the host RecyclerView if measured + * as UNSPECIFIED. AT_MOST measurements will be treated as EXACTLY and the RecyclerView + * will consume all available space. + * + * @param recycler Recycler + * @param state Transient state of RecyclerView + * @param widthSpec Width {@link android.view.View.MeasureSpec} + * @param heightSpec Height {@link android.view.View.MeasureSpec} + * @see #isAutoMeasureEnabled() + * @see #setMeasuredDimension(int, int) + */ + public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec, + int heightSpec) { + mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); } - boolean hasAnyOfTheFlags(int flags) { - return (mFlags & flags) != 0; + /** + * {@link View#setMeasuredDimension(int, int) Set the measured dimensions} of the + * host RecyclerView. + * + * @param widthSize Measured width + * @param heightSize Measured height + */ + public void setMeasuredDimension(int widthSize, int heightSize) { + mRecyclerView.setMeasuredDimension(widthSize, heightSize); } - boolean isTmpDetached() { - return (mFlags & FLAG_TMP_DETACHED) != 0; + /** + * @return The host RecyclerView's {@link View#getMinimumWidth()} + */ + @Px + public int getMinimumWidth() { + return ViewCompat.getMinimumWidth(mRecyclerView); } - boolean isAttachedToTransitionOverlay() { - return itemView.getParent() != null && itemView.getParent() != mOwnerRecyclerView; + /** + * @return The host RecyclerView's {@link View#getMinimumHeight()} + */ + @Px + public int getMinimumHeight() { + return ViewCompat.getMinimumHeight(mRecyclerView); } - boolean isAdapterPositionUnknown() { - return (mFlags & FLAG_ADAPTER_POSITION_UNKNOWN) != 0 || isInvalid(); + /** + *

    Called when the LayoutManager should save its state. This is a good time to save your + * scroll position, configuration and anything else that may be required to restore the same + * layout state if the LayoutManager is recreated.

    + *

    RecyclerView does NOT verify if the LayoutManager has changed between state save and + * restore. This will let you share information between your LayoutManagers but it is also + * your responsibility to make sure they use the same parcelable class.

    + * + * @return Necessary information for LayoutManager to be able to restore its state + */ + @Nullable + public Parcelable onSaveInstanceState() { + return null; } - void setFlags(int flags, int mask) { - mFlags = (mFlags & ~mask) | (flags & mask); - } + /** + * Called when the RecyclerView is ready to restore the state based on a previous + * RecyclerView. + * + * Notice that this might happen after an actual layout, based on how Adapter prefers to + * restore State. See {@link Adapter#getStateRestorationPolicy()} for more information. + * + * @param state The parcelable that was returned by the previous LayoutManager's + * {@link #onSaveInstanceState()} method. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onRestoreInstanceState(Parcelable state) { - void addFlags(int flags) { - mFlags |= flags; } - void addChangePayload(Object payload) { - if (payload == null) { - addFlags(FLAG_ADAPTER_FULLUPDATE); - } else if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) { - createPayloadsIfNeeded(); - mPayloads.add(payload); + void stopSmoothScroller() { + if (mSmoothScroller != null) { + mSmoothScroller.stop(); } } - private void createPayloadsIfNeeded() { - if (mPayloads == null) { - mPayloads = new ArrayList<>(); - mUnmodifiedPayloads = Collections.unmodifiableList(mPayloads); + void onSmoothScrollerStopped(SmoothScroller smoothScroller) { + if (mSmoothScroller == smoothScroller) { + mSmoothScroller = null; } } - void clearPayload() { - if (mPayloads != null) { - mPayloads.clear(); - } - mFlags = mFlags & ~FLAG_ADAPTER_FULLUPDATE; + /** + * RecyclerView calls this method to notify LayoutManager that scroll state has changed. + * + * @param state The new scroll state for RecyclerView + */ + public void onScrollStateChanged(int state) { } - List getUnmodifiedPayloads() { - if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) { - if (mPayloads == null || mPayloads.size() == 0) { - // Initial state, no update being called. - return FULLUPDATE_PAYLOADS; + /** + * Removes all views and recycles them using the given recycler. + *

    + * If you want to clean cached views as well, you should call {@link Recycler#clear()} too. + *

    + * If a View is marked as "ignored", it is not removed nor recycled. + * + * @param recycler Recycler to use to recycle children + * @see #removeAndRecycleView(View, Recycler) + * @see #removeAndRecycleViewAt(int, Recycler) + * @see #ignoreView(View) + */ + public void removeAndRecycleAllViews(@NonNull Recycler recycler) { + for (int i = getChildCount() - 1; i >= 0; i--) { + final View view = getChildAt(i); + if (!getChildViewHolderInt(view).shouldIgnore()) { + removeAndRecycleViewAt(i, recycler); } - // there are none-null payloads - return mUnmodifiedPayloads; - } else { - // a full update has been called. - return FULLUPDATE_PAYLOADS; } } - void resetInternal() { - mFlags = 0; - mPosition = NO_POSITION; - mOldPosition = NO_POSITION; - mItemId = NO_ID; - mPreLayoutPosition = NO_POSITION; - mIsRecyclableCount = 0; - mShadowedHolder = null; - mShadowingHolder = null; - clearPayload(); - mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; - mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET; - clearNestedRecyclerViewIfNotNested(this); + // called by accessibility delegate + void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfoCompat info) { + onInitializeAccessibilityNodeInfo(mRecyclerView.mRecycler, mRecyclerView.mState, info); } /** - * Called when the child view enters the hidden state + * Called by the AccessibilityDelegate when the information about the current layout should + * be populated. + *

    + * Default implementation adds a {@link + * androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat}. + *

    + * You should override + * {@link #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)}, + * {@link #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)}, + * {@link #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State)} and + * {@link #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State)} for + * more accurate accessibility information. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param info The info that should be filled by the LayoutManager + * @see View#onInitializeAccessibilityNodeInfo( + *android.view.accessibility.AccessibilityNodeInfo) + * @see #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State) + * @see #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State) + * @see #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State) + * @see #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State) */ - void onEnteredHiddenState(RecyclerView parent) { - // While the view item is in hidden state, make it invisible for the accessibility. - if (mPendingAccessibilityState != PENDING_ACCESSIBILITY_STATE_NOT_SET) { - mWasImportantForAccessibilityBeforeHidden = mPendingAccessibilityState; - } else { - mWasImportantForAccessibilityBeforeHidden = - ViewCompat.getImportantForAccessibility(itemView); + public void onInitializeAccessibilityNodeInfo(@NonNull Recycler recycler, + @NonNull State state, @NonNull AccessibilityNodeInfoCompat info) { + if (mRecyclerView.canScrollVertically(-1) || mRecyclerView.canScrollHorizontally(-1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + info.setScrollable(true); } - parent.setChildImportantForAccessibilityInternal(this, - ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - } - - /** - * Called when the child view leaves the hidden state - */ - void onLeftHiddenState(RecyclerView parent) { - parent.setChildImportantForAccessibilityInternal(this, - mWasImportantForAccessibilityBeforeHidden); - mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; - } - - @NonNull - @Override - public String toString() { - String className = - getClass().isAnonymousClass() ? "ViewHolder" : getClass().getSimpleName(); - StringBuilder sb = new StringBuilder(className + "{" - + Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId - + ", oldPos=" + mOldPosition + ", pLpos:" + mPreLayoutPosition); - if (isScrap()) { - sb.append(" scrap ") - .append(mInChangeScrap ? "[changeScrap]" : "[attachedScrap]"); + if (mRecyclerView.canScrollVertically(1) || mRecyclerView.canScrollHorizontally(1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + info.setScrollable(true); } - if (isInvalid()) sb.append(" invalid"); - if (!isBound()) sb.append(" unbound"); - if (needsUpdate()) sb.append(" update"); - if (isRemoved()) sb.append(" removed"); - if (shouldIgnore()) sb.append(" ignored"); - if (isTmpDetached()) sb.append(" tmpDetached"); - if (!isRecyclable()) - sb.append(" not recyclable(").append(mIsRecyclableCount).append(")"); - if (isAdapterPositionUnknown()) sb.append(" undefined adapter position"); + final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo = + AccessibilityNodeInfoCompat.CollectionInfoCompat + .obtain(getRowCountForAccessibility(recycler, state), + getColumnCountForAccessibility(recycler, state), + isLayoutHierarchical(recycler, state), + getSelectionModeForAccessibility(recycler, state)); + info.setCollectionInfo(collectionInfo); + } - if (itemView.getParent() == null) sb.append(" no parent"); - sb.append("}"); - return sb.toString(); + // called by accessibility delegate + public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) { + onInitializeAccessibilityEvent(mRecyclerView.mRecycler, mRecyclerView.mState, event); } /** - * Informs the recycler whether this item can be recycled. Views which are not - * recyclable will not be reused for other items until setIsRecyclable() is - * later set to true. Calls to setIsRecyclable() should always be paired (one - * call to setIsRecyclabe(false) should always be matched with a later call to - * setIsRecyclable(true)). Pairs of calls may be nested, as the state is internally - * reference-counted. + * Called by the accessibility delegate to initialize an accessibility event. + *

    + * Default implementation adds item count and scroll information to the event. * - * @param recyclable Whether this item is available to be recycled. Default value - * is true. - * @see #isRecyclable() + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param event The event instance to initialize + * @see View#onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent) */ - public final void setIsRecyclable(boolean recyclable) { - mIsRecyclableCount = recyclable ? mIsRecyclableCount - 1 : mIsRecyclableCount + 1; - if (mIsRecyclableCount < 0) { - mIsRecyclableCount = 0; - if (DEBUG) { - throw new RuntimeException("isRecyclable decremented below 0: " - + "unmatched pair of setIsRecyable() calls for " + this); - } - Log.e(VIEW_LOG_TAG, "isRecyclable decremented below 0: " - + "unmatched pair of setIsRecyable() calls for " + this); - } else if (!recyclable && mIsRecyclableCount == 1) { - mFlags |= FLAG_NOT_RECYCLABLE; - } else if (recyclable && mIsRecyclableCount == 0) { - mFlags &= ~FLAG_NOT_RECYCLABLE; + public void onInitializeAccessibilityEvent(@NonNull Recycler recycler, @NonNull State state, + @NonNull AccessibilityEvent event) { + if (mRecyclerView == null || event == null) { + return; } - if (DEBUG) { - Log.d(TAG, "setIsRecyclable val:" + recyclable + ":" + this); + event.setScrollable(mRecyclerView.canScrollVertically(1) + || mRecyclerView.canScrollVertically(-1) + || mRecyclerView.canScrollHorizontally(-1) + || mRecyclerView.canScrollHorizontally(1)); + + if (mRecyclerView.mAdapter != null) { + event.setItemCount(mRecyclerView.mAdapter.getItemCount()); + } + } + + // called by accessibility delegate + void onInitializeAccessibilityNodeInfoForItem(View host, AccessibilityNodeInfoCompat info) { + final ViewHolder vh = getChildViewHolderInt(host); + // avoid trying to create accessibility node info for removed children + if (vh != null && !vh.isRemoved() && !mChildHelper.isHidden(vh.itemView)) { + onInitializeAccessibilityNodeInfoForItem(mRecyclerView.mRecycler, + mRecyclerView.mState, host, info); } } /** - * @return true if this item is available to be recycled, false otherwise. - * @see #setIsRecyclable(boolean) + * Called by the AccessibilityDelegate when the accessibility information for a specific + * item should be populated. + *

    + * Default implementation adds basic positioning information about the item. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param host The child for which accessibility node info should be populated + * @param info The info to fill out about the item + * @see android.widget.AbsListView#onInitializeAccessibilityNodeInfoForItem(View, int, + * android.view.accessibility.AccessibilityNodeInfo) */ - public final boolean isRecyclable() { - return (mFlags & FLAG_NOT_RECYCLABLE) == 0 - && !ViewCompat.hasTransientState(itemView); + public void onInitializeAccessibilityNodeInfoForItem(@NonNull Recycler recycler, + @NonNull State state, @NonNull View host, + @NonNull AccessibilityNodeInfoCompat info) { + int rowIndexGuess = canScrollVertically() ? getPosition(host) : 0; + int columnIndexGuess = canScrollHorizontally() ? getPosition(host) : 0; + final AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo = + AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(rowIndexGuess, 1, + columnIndexGuess, 1, false, false); + info.setCollectionItemInfo(itemInfo); } /** - * Returns whether we have animations referring to this view holder or not. - * This is similar to isRecyclable flag but does not check transient state. + * A LayoutManager can call this method to force RecyclerView to run simple animations in + * the next layout pass, even if there is not any trigger to do so. (e.g. adapter data + * change). + *

    + * Note that, calling this method will not guarantee that RecyclerView will run animations + * at all. For example, if there is not any {@link ItemAnimator} set, RecyclerView will + * not run any animations but will still clear this flag after the layout is complete. */ - boolean shouldBeKeptAsChild() { - return (mFlags & FLAG_NOT_RECYCLABLE) != 0; + public void requestSimpleAnimationsInNextLayout() { + mRequestedSimpleAnimations = true; } /** - * @return True if ViewHolder is not referenced by RecyclerView animations but has - * transient state which will prevent it from being recycled. + * Returns the selection mode for accessibility. Should be + * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE}, + * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_SINGLE} or + * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_MULTIPLE}. + *

    + * Default implementation returns + * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE}. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return Selection mode for accessibility. Default implementation returns + * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE}. */ - boolean doesTransientStatePreventRecycling() { - return (mFlags & FLAG_NOT_RECYCLABLE) == 0 && ViewCompat.hasTransientState(itemView); + public int getSelectionModeForAccessibility(@NonNull Recycler recycler, + @NonNull State state) { + return AccessibilityNodeInfoCompat.CollectionInfoCompat.SELECTION_MODE_NONE; } - boolean isUpdated() { - return (mFlags & FLAG_UPDATE) != 0; + /** + * Returns the number of rows for accessibility. + *

    + * Default implementation returns the number of items in the adapter if LayoutManager + * supports vertical scrolling or 1 if LayoutManager does not support vertical + * scrolling. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return The number of rows in LayoutManager for accessibility. + */ + public int getRowCountForAccessibility(@NonNull Recycler recycler, @NonNull State state) { + if (mRecyclerView == null || mRecyclerView.mAdapter == null) { + return 1; + } + return canScrollVertically() ? mRecyclerView.mAdapter.getItemCount() : 1; } - } - - /** - * {@link android.view.ViewGroup.MarginLayoutParams LayoutParams} subclass for children of - * {@link RecyclerView}. Custom {@link LayoutManager layout managers} are encouraged - * to create their own subclass of this LayoutParams class - * to store any additional required per-child view metadata about the layout. - */ - public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams { - final Rect mDecorInsets = new Rect(); - ViewHolder mViewHolder; - boolean mInsetsDirty = true; - // Flag is set to true if the view is bound while it is detached from RV. - // In this case, we need to manually call invalidate after view is added to guarantee that - // invalidation is populated through the View hierarchy - boolean mPendingInvalidate; - public LayoutParams(@SuppressLint("UnknownNullness") Context c, @SuppressLint("UnknownNullness") AttributeSet attrs) { - super(c, attrs); + /** + * Returns the number of columns for accessibility. + *

    + * Default implementation returns the number of items in the adapter if LayoutManager + * supports horizontal scrolling or 1 if LayoutManager does not support horizontal + * scrolling. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return The number of rows in LayoutManager for accessibility. + */ + public int getColumnCountForAccessibility(@NonNull Recycler recycler, + @NonNull State state) { + if (mRecyclerView == null || mRecyclerView.mAdapter == null) { + return 1; + } + return canScrollHorizontally() ? mRecyclerView.mAdapter.getItemCount() : 1; } - public LayoutParams(int width, int height) { - super(width, height); + /** + * Returns whether layout is hierarchical or not to be used for accessibility. + *

    + * Default implementation returns false. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return True if layout is hierarchical. + */ + public boolean isLayoutHierarchical(@NonNull Recycler recycler, @NonNull State state) { + return false; } - public LayoutParams(@SuppressLint("UnknownNullness") MarginLayoutParams source) { - super(source); + // called by accessibility delegate + boolean performAccessibilityAction(int action, @Nullable Bundle args) { + return performAccessibilityAction(mRecyclerView.mRecycler, mRecyclerView.mState, + action, args); } - public LayoutParams(@SuppressLint("UnknownNullness") ViewGroup.LayoutParams source) { - super(source); + /** + * Called by AccessibilityDelegate when an action is requested from the RecyclerView. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param action The action to perform + * @param args Optional action arguments + * @see View#performAccessibilityAction(int, android.os.Bundle) + */ + public boolean performAccessibilityAction(@NonNull Recycler recycler, @NonNull State state, + int action, @Nullable Bundle args) { + if (mRecyclerView == null) { + return false; + } + int vScroll = 0, hScroll = 0; + int height = getHeight(); + int width = getWidth(); + Rect rect = new Rect(); + // Gets the visible rect on the screen except for the rotation or scale cases which + // might affect the result. + if (mRecyclerView.getMatrix().isIdentity() && mRecyclerView.getGlobalVisibleRect( + rect)) { + height = rect.height(); + width = rect.width(); + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: + if (mRecyclerView.canScrollVertically(-1)) { + vScroll = -(height - getPaddingTop() - getPaddingBottom()); + } + if (mRecyclerView.canScrollHorizontally(-1)) { + hScroll = -(width - getPaddingLeft() - getPaddingRight()); + } + break; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: + if (mRecyclerView.canScrollVertically(1)) { + vScroll = height - getPaddingTop() - getPaddingBottom(); + } + if (mRecyclerView.canScrollHorizontally(1)) { + hScroll = width - getPaddingLeft() - getPaddingRight(); + } + break; + } + if (vScroll == 0 && hScroll == 0) { + return false; + } + mRecyclerView.smoothScrollBy(hScroll, vScroll, null, UNDEFINED_DURATION, true); + return true; } - public LayoutParams(@SuppressLint("UnknownNullness") LayoutParams source) { - super((ViewGroup.LayoutParams) source); + // called by accessibility delegate + boolean performAccessibilityActionForItem(@NonNull View view, int action, + @Nullable Bundle args) { + return performAccessibilityActionForItem(mRecyclerView.mRecycler, mRecyclerView.mState, + view, action, args); } /** - * Returns true if the view this LayoutParams is attached to needs to have its content - * updated from the corresponding adapter. + * Called by AccessibilityDelegate when an accessibility action is requested on one of the + * children of LayoutManager. + *

    + * Default implementation does not do anything. * - * @return true if the view should have its content updated + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param view The child view on which the action is performed + * @param action The action to perform + * @param args Optional action arguments + * @return true if action is handled + * @see View#performAccessibilityAction(int, android.os.Bundle) */ - public boolean viewNeedsUpdate() { - return mViewHolder.needsUpdate(); + public boolean performAccessibilityActionForItem(@NonNull Recycler recycler, + @NonNull State state, @NonNull View view, int action, @Nullable Bundle args) { + return false; } /** - * Returns true if the view this LayoutParams is attached to is now representing - * potentially invalid data. A LayoutManager should scrap/recycle it. + * Parse the xml attributes to get the most common properties used by layout managers. * - * @return true if the view is invalid + * {@link android.R.attr#orientation} + * {@link androidx.viewpager2.R.attr#spanCount} + * {@link androidx.viewpager2.R.attr#reverseLayout} + * {@link androidx.viewpager2.R.attr#stackFromEnd} + * + * @return an object containing the properties as specified in the attrs. */ - public boolean isViewInvalid() { - return mViewHolder.isInvalid(); + public static Properties getProperties(@NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + Properties properties = new Properties(); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView, + defStyleAttr, defStyleRes); + properties.orientation = a.getInt(R.styleable.RecyclerView_android_orientation, + DEFAULT_ORIENTATION); + properties.spanCount = a.getInt(R.styleable.RecyclerView_spanCount, 1); + properties.reverseLayout = a.getBoolean(R.styleable.RecyclerView_reverseLayout, false); + properties.stackFromEnd = a.getBoolean(R.styleable.RecyclerView_stackFromEnd, false); + a.recycle(); + return properties; } - /** - * Returns true if the adapter data item corresponding to the view this LayoutParams - * is attached to has been removed from the data set. A LayoutManager may choose to - * treat it differently in order to animate its outgoing or disappearing state. - * - * @return true if the item the view corresponds to was removed from the data set - */ - public boolean isItemRemoved() { - return mViewHolder.isRemoved(); + void setExactMeasureSpecsFrom(RecyclerView recyclerView) { + setMeasureSpecs( + MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(recyclerView.getHeight(), MeasureSpec.EXACTLY) + ); } /** - * Returns true if the adapter data item corresponding to the view this LayoutParams - * is attached to has been changed in the data set. A LayoutManager may choose to - * treat it differently in order to animate its changing state. - * - * @return true if the item the view corresponds to was changed in the data set + * Internal API to allow LayoutManagers to be measured twice. + *

    + * This is not public because LayoutManagers should be able to handle their layouts in one + * pass but it is very convenient to make existing LayoutManagers support wrapping content + * when both orientations are undefined. + *

    + * This API will be removed after default LayoutManagers properly implement wrap content in + * non-scroll orientation. */ - public boolean isItemChanged() { - return mViewHolder.isUpdated(); + boolean shouldMeasureTwice() { + return false; + } + + boolean hasFlexibleChildInBothOrientations() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final ViewGroup.LayoutParams lp = child.getLayoutParams(); + if (lp.width < 0 && lp.height < 0) { + return true; + } + } + return false; } /** - * @deprecated use {@link #getViewLayoutPosition()} or {@link #getViewAdapterPosition()} + * Some general properties that a LayoutManager may want to use. */ - @Deprecated - public int getViewPosition() { - return mViewHolder.getPosition(); + public static class Properties { + /** {@link android.R.attr#orientation} */ + public int orientation; + /** {@link androidx.viewpager2.R.attr#spanCount} */ + public int spanCount; + /** {@link androidx.viewpager2.R.attr#reverseLayout} */ + public boolean reverseLayout; + /** {@link androidx.viewpager2.R.attr#stackFromEnd} */ + public boolean stackFromEnd; } + } + /** + * An ItemDecoration allows the application to add a special drawing and layout offset + * to specific item views from the adapter's data set. This can be useful for drawing dividers + * between items, highlights, visual grouping boundaries and more. + * + *

    All ItemDecorations are drawn in the order they were added, before the item + * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()} + * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView, + * RecyclerView.State)}.

    + */ + public abstract static class ItemDecoration { /** - * Returns the adapter position that the view this LayoutParams is attached to corresponds - * to as of latest layout calculation. + * Draw any appropriate decorations into the Canvas supplied to the RecyclerView. + * Any content drawn by this method will be drawn before the item views are drawn, + * and will thus appear underneath the views. * - * @return the adapter position this view as of latest layout pass + * @param c Canvas to draw into + * @param parent RecyclerView this ItemDecoration is drawing into + * @param state The current state of RecyclerView */ - public int getViewLayoutPosition() { - return mViewHolder.getLayoutPosition(); + public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) { + onDraw(c, parent); } /** - * @deprecated This method is confusing when nested adapters are used. - * If you are calling from the context of an {@link Adapter}, - * use {@link #getBindingAdapterPosition()}. If you need the position that - * {@link RecyclerView} sees, use {@link #getAbsoluteAdapterPosition()}. + * @deprecated Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)} */ @Deprecated - public int getViewAdapterPosition() { - return mViewHolder.getBindingAdapterPosition(); + public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) { } /** - * Returns the up-to-date adapter position that the view this LayoutParams is attached to - * corresponds to in the {@link RecyclerView}. If the {@link RecyclerView} has an - * {@link Adapter} that merges other adapters, this position will be with respect to the - * adapter that is assigned to the {@link RecyclerView}. + * Draw any appropriate decorations into the Canvas supplied to the RecyclerView. + * Any content drawn by this method will be drawn after the item views are drawn + * and will thus appear over the views. * - * @return the up-to-date adapter position this view with respect to the RecyclerView. It - * may return {@link RecyclerView#NO_POSITION} if item represented by this View has been - * removed or - * its up-to-date position cannot be calculated. + * @param c Canvas to draw into + * @param parent RecyclerView this ItemDecoration is drawing into + * @param state The current state of RecyclerView. */ - public int getAbsoluteAdapterPosition() { - return mViewHolder.getAbsoluteAdapterPosition(); + public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, + @NonNull State state) { + onDrawOver(c, parent); } /** - * Returns the up-to-date adapter position that the view this LayoutParams is attached to - * corresponds to with respect to the {@link Adapter} that bound this View. - * - * @return the up-to-date adapter position this view relative to the {@link Adapter} that - * bound this View. It may return {@link RecyclerView#NO_POSITION} if item represented by - * this View has been removed or its up-to-date position cannot be calculated. + * @deprecated Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)} */ - public int getBindingAdapterPosition() { - return mViewHolder.getBindingAdapterPosition(); - } - } - - /** - * Observer base class for watching changes to an {@link Adapter}. - * See {@link Adapter#registerAdapterDataObserver(AdapterDataObserver)}. - */ - public abstract static class AdapterDataObserver { - public void onChanged() { - // Do nothing - } - - public void onItemRangeChanged(int positionStart, int itemCount) { - // do nothing - } - - public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { - // fallback to onItemRangeChanged(positionStart, itemCount) if app - // does not override this method. - onItemRangeChanged(positionStart, itemCount); - } - - public void onItemRangeInserted(int positionStart, int itemCount) { - // do nothing + @Deprecated + public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) { } - public void onItemRangeRemoved(int positionStart, int itemCount) { - // do nothing - } - public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { - // do nothing + /** + * @deprecated Use {@link #getItemOffsets(Rect, View, RecyclerView, State)} + */ + @Deprecated + public void getItemOffsets(@NonNull Rect outRect, int itemPosition, + @NonNull RecyclerView parent) { + outRect.set(0, 0, 0, 0); } /** - * Called when the {@link Adapter.StateRestorationPolicy} of the {@link Adapter} changed. - * When this method is called, the Adapter might be ready to restore its state if it has - * not already been restored. + * Retrieve any offsets for the given item. Each field of outRect specifies + * the number of pixels that the item view should be inset by, similar to padding or margin. + * The default implementation sets the bounds of outRect to 0 and returns. * - * @see Adapter#getStateRestorationPolicy() - * @see Adapter#setStateRestorationPolicy(Adapter.StateRestorationPolicy) + *

    + * If this ItemDecoration does not affect the positioning of item views, it should set + * all four fields of outRect (left, top, right, bottom) to zero + * before returning. + * + *

    + * If you need to access Adapter for additional data, you can call + * {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the + * View. + * + * @param outRect Rect to receive the output. + * @param view The child view to decorate + * @param parent RecyclerView this ItemDecoration is decorating + * @param state The current state of RecyclerView. */ - public void onStateRestorationPolicyChanged() { - // do nothing + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, + @NonNull RecyclerView parent, @NonNull State state) { + getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(), + parent); } } /** - * Base class for smooth scrolling. Handles basic tracking of the target view position and - * provides methods to trigger a programmatic scroll. + * An OnItemTouchListener allows the application to intercept touch events in progress at the + * view hierarchy level of the RecyclerView before those touch events are considered for + * RecyclerView's own scrolling behavior. * - *

    An instance of SmoothScroller is only intended to be used once. You should create a new - * instance for each call to {@link LayoutManager#startSmoothScroll(SmoothScroller)}. + *

    This can be useful for applications that wish to implement various forms of gestural + * manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept + * a touch interaction already in progress even if the RecyclerView is already handling that + * gesture stream itself for the purposes of scrolling.

    * - * @see LinearSmoothScroller + * @see SimpleOnItemTouchListener */ - public abstract static class SmoothScroller { - - private final Action mRecyclingAction; - private int mTargetPosition = NO_POSITION; - private RecyclerView mRecyclerView; - private LayoutManager mLayoutManager; - private boolean mPendingInitialRun; - private boolean mRunning; - private View mTargetView; - private boolean mStarted; - - public SmoothScroller() { - mRecyclingAction = new Action(0, 0); - } - + public interface OnItemTouchListener { /** - * Starts a smooth scroll for the given target position. - *

    In each animation step, {@link RecyclerView} will check - * for the target view and call either - * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or - * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)} until - * SmoothScroller is stopped.

    + * Silently observe and/or take over touch events sent to the RecyclerView + * before they are handled by either the RecyclerView itself or its child views. * - *

    Note that if RecyclerView finds the target view, it will automatically stop the - * SmoothScroller. This does not mean that scroll will stop, it only means it will - * stop calling SmoothScroller in each animation step.

    + *

    The onInterceptTouchEvent methods of each attached OnItemTouchListener will be run + * in the order in which each listener was added, before any other touch processing + * by the RecyclerView itself or child views occurs.

    + * + * @param e MotionEvent describing the touch event. All coordinates are in + * the RecyclerView's coordinate system. + * @return true if this OnItemTouchListener wishes to begin intercepting touch events, false + * to continue with the current behavior and continue observing future events in + * the gesture. */ - void start(RecyclerView recyclerView, LayoutManager layoutManager) { - - // Stop any previous ViewFlinger animations now because we are about to start a new one. - recyclerView.mViewFlinger.stop(); - - if (mStarted) { - Log.w(TAG, "An instance of " + getClass().getSimpleName() + " was started " - + "more than once. Each instance of" + getClass().getSimpleName() + " " - + "is intended to only be used once. You should create a new instance for " - + "each use."); - } - - mRecyclerView = recyclerView; - mLayoutManager = layoutManager; - if (mTargetPosition == NO_POSITION) { - throw new IllegalArgumentException("Invalid target position"); - } - mRecyclerView.mState.mTargetPosition = mTargetPosition; - mRunning = true; - mPendingInitialRun = true; - mTargetView = findViewByPosition(getTargetPosition()); - onStart(); - mRecyclerView.mViewFlinger.postOnAnimation(); + boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e); - mStarted = true; - } + /** + * Process a touch event as part of a gesture that was claimed by returning true from + * a previous call to {@link #onInterceptTouchEvent}. + * + * @param e MotionEvent describing the touch event. All coordinates are in + * the RecyclerView's coordinate system. + */ + void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e); /** - * Compute the scroll vector for a given target position. - *

    - * This method can return null if the layout manager cannot calculate a scroll vector - * for the given position (e.g. it has no current scroll position). + * Called when a child of RecyclerView does not want RecyclerView and its ancestors to + * intercept touch events with + * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}. * - * @param targetPosition the position to which the scroller is scrolling - * @return the scroll vector for a given target position + * @param disallowIntercept True if the child does not want the parent to + * intercept touch events. + * @see ViewParent#requestDisallowInterceptTouchEvent(boolean) */ - @Nullable - public PointF computeScrollVectorForPosition(int targetPosition) { - LayoutManager layoutManager = getLayoutManager(); - if (layoutManager instanceof ScrollVectorProvider) { - return ((ScrollVectorProvider) layoutManager) - .computeScrollVectorForPosition(targetPosition); - } - Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager" - + " does not implement " + ScrollVectorProvider.class.getCanonicalName()); - return null; + void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept); + } + + /** + * An implementation of {@link RecyclerView.OnItemTouchListener} that has empty method bodies + * and default return values. + *

    + * You may prefer to extend this class if you don't need to override all methods. Another + * benefit of using this class is future compatibility. As the interface may change, we'll + * always provide a default implementation on this class so that your code won't break when + * you update to a new version of the support library. + */ + public static class SimpleOnItemTouchListener implements RecyclerView.OnItemTouchListener { + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + return false; } - /** - * @return The LayoutManager to which this SmoothScroller is attached. Will return - * null after the SmoothScroller is stopped. - */ - @Nullable - public LayoutManager getLayoutManager() { - return mLayoutManager; + @Override + public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { } - /** - * Stops running the SmoothScroller in each animation callback. Note that this does not - * cancel any existing {@link Action} updated by - * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or - * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)}. - */ - protected final void stop() { - if (!mRunning) { - return; - } - mRunning = false; - onStop(); - mRecyclerView.mState.mTargetPosition = NO_POSITION; - mTargetView = null; - mTargetPosition = NO_POSITION; - mPendingInitialRun = false; - // trigger a cleanup - mLayoutManager.onSmoothScrollerStopped(this); - // clear references to avoid any potential leak by a custom smooth scroller - mLayoutManager = null; - mRecyclerView = null; + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } + } + + /** + * An OnScrollListener can be added to a RecyclerView to receive messages when a scrolling event + * has occurred on that RecyclerView. + *

    + * + * @see RecyclerView#addOnScrollListener(OnScrollListener) + * @see RecyclerView#clearOnChildAttachStateChangeListeners() + */ + public abstract static class OnScrollListener { /** - * Returns true if SmoothScroller has been started but has not received the first - * animation - * callback yet. + * Callback method to be invoked when RecyclerView's scroll state changes. * - * @return True if this SmoothScroller is waiting to start + * @param recyclerView The RecyclerView whose scroll state has changed. + * @param newState The updated scroll state. One of {@link #SCROLL_STATE_IDLE}, + * {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}. */ - public boolean isPendingInitialRun() { - return mPendingInitialRun; + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { } /** - * @return True if SmoothScroller is currently active + * Callback method to be invoked when the RecyclerView has been scrolled. This will be + * called after the scroll has completed. + *

    + * This callback will also be called if visible item range changes after a layout + * calculation. In that case, dx and dy will be 0. + * + * @param recyclerView The RecyclerView which scrolled. + * @param dx The amount of horizontal scroll. + * @param dy The amount of vertical scroll. */ - public boolean isRunning() { - return mRunning; + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { } + } + + /** + * A RecyclerListener can be set on a RecyclerView to receive messages whenever + * a view is recycled. + * + * @see RecyclerView#setRecyclerListener(RecyclerListener) + */ + public interface RecyclerListener { /** - * Returns the adapter position of the target item + * This method is called whenever the view in the ViewHolder is recycled. * - * @return Adapter position of the target item or - * {@link RecyclerView#NO_POSITION} if no target view is set. + * RecyclerView calls this method right before clearing ViewHolder's internal data and + * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information + * before being recycled, you can call {@link ViewHolder#getBindingAdapterPosition()} to get + * its adapter position. + * + * @param holder The ViewHolder containing the view that was recycled */ - public int getTargetPosition() { - return mTargetPosition; - } + void onViewRecycled(@NonNull ViewHolder holder); + } - public void setTargetPosition(int targetPosition) { - mTargetPosition = targetPosition; - } + /** + * A Listener interface that can be attached to a RecylcerView to get notified + * whenever a ViewHolder is attached to or detached from RecyclerView. + */ + public interface OnChildAttachStateChangeListener { - void onAnimation(int dx, int dy) { - RecyclerView recyclerView = mRecyclerView; - if (mTargetPosition == NO_POSITION || recyclerView == null) { - stop(); - } + /** + * Called when a view is attached to the RecyclerView. + * + * @param view The View which is attached to the RecyclerView + */ + void onChildViewAttachedToWindow(@NonNull View view); - // The following if block exists to have the LayoutManager scroll 1 pixel in the correct - // direction in order to cause the LayoutManager to draw two pages worth of views so - // that the target view may be found before scrolling any further. This is done to - // prevent an initial scroll distance from scrolling past the view, which causes a - // jittery looking animation. - if (mPendingInitialRun && mTargetView == null && mLayoutManager != null) { - PointF pointF = computeScrollVectorForPosition(mTargetPosition); - if (pointF != null && (pointF.x != 0 || pointF.y != 0)) { - recyclerView.scrollStep( - (int) Math.signum(pointF.x), - (int) Math.signum(pointF.y), - null); - } - } + /** + * Called when a view is detached from RecyclerView. + * + * @param view The View which is being detached from the RecyclerView + */ + void onChildViewDetachedFromWindow(@NonNull View view); + } - mPendingInitialRun = false; + /** + * A ViewHolder describes an item view and metadata about its place within the RecyclerView. + * + *

    {@link Adapter} implementations should subclass ViewHolder and add fields for caching + * potentially expensive {@link View#findViewById(int)} results.

    + * + *

    While {@link LayoutParams} belong to the {@link LayoutManager}, + * {@link ViewHolder ViewHolders} belong to the adapter. Adapters should feel free to use + * their own custom ViewHolder implementations to store data that makes binding view contents + * easier. Implementations should assume that individual item views will hold strong references + * to ViewHolder objects and that RecyclerView instances may hold + * strong references to extra off-screen item views for caching purposes

    + */ + public abstract static class ViewHolder { + @NonNull + public final View itemView; + WeakReference mNestedRecyclerView; + int mPosition = NO_POSITION; + int mOldPosition = NO_POSITION; + long mItemId = NO_ID; + int mItemViewType = INVALID_TYPE; + int mPreLayoutPosition = NO_POSITION; - if (mTargetView != null) { - // verify target position - if (getChildPosition(mTargetView) == mTargetPosition) { - onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction); - mRecyclingAction.runIfNecessary(recyclerView); - stop(); - } else { - Log.e(TAG, "Passed over target position while smooth scrolling."); - mTargetView = null; - } - } - if (mRunning) { - onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction); - boolean hadJumpTarget = mRecyclingAction.hasJumpTarget(); - mRecyclingAction.runIfNecessary(recyclerView); - if (hadJumpTarget) { - // It is not stopped so needs to be restarted - if (mRunning) { - mPendingInitialRun = true; - recyclerView.mViewFlinger.postOnAnimation(); - } - } - } - } + // The item that this holder is shadowing during an item change event/animation + ViewHolder mShadowedHolder = null; + // The item that is shadowing this holder during an item change event/animation + ViewHolder mShadowingHolder = null; /** - * @see RecyclerView#getChildLayoutPosition(android.view.View) + * This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType + * are all valid. */ - public int getChildPosition(View view) { - return mRecyclerView.getChildLayoutPosition(view); - } + static final int FLAG_BOUND = 1 << 0; /** - * @see RecyclerView.LayoutManager#getChildCount() + * The data this ViewHolder's view reflects is stale and needs to be rebound + * by the adapter. mPosition and mItemId are consistent. */ - public int getChildCount() { - return mRecyclerView.mLayout.getChildCount(); - } + static final int FLAG_UPDATE = 1 << 1; /** - * @see RecyclerView.LayoutManager#findViewByPosition(int) + * This ViewHolder's data is invalid. The identity implied by mPosition and mItemId + * are not to be trusted and may no longer match the item view type. + * This ViewHolder must be fully rebound to different data. */ - public View findViewByPosition(int position) { - return mRecyclerView.mLayout.findViewByPosition(position); - } + static final int FLAG_INVALID = 1 << 2; /** - * @see RecyclerView#scrollToPosition(int) - * @deprecated Use {@link Action#jumpTo(int)}. + * This ViewHolder points at data that represents an item previously removed from the + * data set. Its view may still be used for things like outgoing animations. */ - @Deprecated - public void instantScrollToPosition(int position) { - mRecyclerView.scrollToPosition(position); - } + static final int FLAG_REMOVED = 1 << 3; - protected void onChildAttachedToWindow(View child) { - if (getChildPosition(child) == getTargetPosition()) { - mTargetView = child; - if (DEBUG) { - Log.d(TAG, "smooth scroll target view has been attached"); - } - } - } + /** + * This ViewHolder should not be recycled. This flag is set via setIsRecyclable() + * and is intended to keep views around during animations. + */ + static final int FLAG_NOT_RECYCLABLE = 1 << 4; /** - * Normalizes the vector. - * - * @param scrollVector The vector that points to the target scroll position + * This ViewHolder is returned from scrap which means we are expecting an addView call + * for this itemView. When returned from scrap, ViewHolder stays in the scrap list until + * the end of the layout pass and then recycled by RecyclerView if it is not added back to + * the RecyclerView. */ - protected void normalize(@NonNull PointF scrollVector) { - float magnitude = (float) Math.sqrt(scrollVector.x * scrollVector.x - + scrollVector.y * scrollVector.y); - scrollVector.x /= magnitude; - scrollVector.y /= magnitude; - } + static final int FLAG_RETURNED_FROM_SCRAP = 1 << 5; /** - * Called when smooth scroll is started. This might be a good time to do setup. + * This ViewHolder is fully managed by the LayoutManager. We do not scrap, recycle or remove + * it unless LayoutManager is replaced. + * It is still fully visible to the LayoutManager. */ - protected abstract void onStart(); + static final int FLAG_IGNORE = 1 << 7; /** - * Called when smooth scroller is stopped. This is a good place to cleanup your state etc. - * - * @see #stop() + * When the View is detached form the parent, we set this flag so that we can take correct + * action when we need to remove it or add it back. + */ + static final int FLAG_TMP_DETACHED = 1 << 8; + + /** + * Set when we can no longer determine the adapter position of this ViewHolder until it is + * rebound to a new position. It is different than FLAG_INVALID because FLAG_INVALID is + * set even when the type does not match. Also, FLAG_ADAPTER_POSITION_UNKNOWN is set as soon + * as adapter notification arrives vs FLAG_INVALID is set lazily before layout is + * re-calculated. */ - protected abstract void onStop(); + static final int FLAG_ADAPTER_POSITION_UNKNOWN = 1 << 9; /** - *

    RecyclerView will call this method each time it scrolls until it can find the target - * position in the layout.

    - *

    SmoothScroller should check dx, dy and if scroll should be changed, update the - * provided {@link Action} to define the next scroll.

    - * - * @param dx Last scroll amount horizontally - * @param dy Last scroll amount vertically - * @param state Transient state of RecyclerView - * @param action If you want to trigger a new smooth scroll and cancel the previous one, - * update this object. + * Set when a addChangePayload(null) is called */ - protected abstract void onSeekTargetStep(@Px int dx, @Px int dy, @NonNull State state, - @NonNull Action action); + static final int FLAG_ADAPTER_FULLUPDATE = 1 << 10; /** - * Called when the target position is laid out. This is the last callback SmoothScroller - * will receive and it should update the provided {@link Action} to define the scroll - * details towards the target view. - * - * @param targetView The view element which render the target position. - * @param state Transient state of RecyclerView - * @param action Action instance that you should update to define final scroll action - * towards the targetView + * Used by ItemAnimator when a ViewHolder's position changes */ - protected abstract void onTargetFound(@NonNull View targetView, @NonNull State state, - @NonNull Action action); + static final int FLAG_MOVED = 1 << 11; /** - * An interface which is optionally implemented by custom {@link RecyclerView.LayoutManager} - * to provide a hint to a {@link SmoothScroller} about the location of the target position. + * Used by ItemAnimator when a ViewHolder appears in pre-layout */ - public interface ScrollVectorProvider { - /** - * Should calculate the vector that points to the direction where the target position - * can be found. - *

    - * This method is used by the {@link LinearSmoothScroller} to initiate a scroll towards - * the target position. - *

    - * The magnitude of the vector is not important. It is always normalized before being - * used by the {@link LinearSmoothScroller}. - *

    - * LayoutManager should not check whether the position exists in the adapter or not. - * - * @param targetPosition the target position to which the returned vector should point - * @return the scroll vector for a given position. - */ - @Nullable - PointF computeScrollVectorForPosition(int targetPosition); - } + static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12; + + static final int PENDING_ACCESSIBILITY_STATE_NOT_SET = -1; /** - * Holds information about a smooth scroll request by a {@link SmoothScroller}. + * Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from + * hidden list (as if it was scrap) without being recycled in between. + * + * When a ViewHolder is hidden, there are 2 paths it can be re-used: + * a) Animation ends, view is recycled and used from the recycle pool. + * b) LayoutManager asks for the View for that position while the ViewHolder is hidden. + * + * This flag is used to represent "case b" where the ViewHolder is reused without being + * recycled (thus "bounced" from the hidden list). This state requires special handling + * because the ViewHolder must be added to pre layout maps for animations as if it was + * already there. */ - public static class Action { + static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13; - public static final int UNDEFINED_DURATION = RecyclerView.UNDEFINED_DURATION; + int mFlags; - private int mDx; + private static final List FULLUPDATE_PAYLOADS = Collections.emptyList(); - private int mDy; + List mPayloads = null; + List mUnmodifiedPayloads = null; - private int mDuration; + private int mIsRecyclableCount = 0; - private int mJumpToPosition = NO_POSITION; + // If non-null, view is currently considered scrap and may be reused for other data by the + // scrap container. + Recycler mScrapContainer = null; + // Keeps whether this ViewHolder lives in Change scrap or Attached scrap + boolean mInChangeScrap = false; - private Interpolator mInterpolator; + // Saves isImportantForAccessibility value for the view item while it's in hidden state and + // marked as unimportant for accessibility. + private int mWasImportantForAccessibilityBeforeHidden = + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + // set if we defer the accessibility state change of the view holder + @VisibleForTesting + int mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET; - private boolean mChanged; + /** + * Is set when VH is bound from the adapter and cleaned right before it is sent to + * {@link RecycledViewPool}. + */ + RecyclerView mOwnerRecyclerView; - // we track this variable to inform custom implementer if they are updating the action - // in every animation callback - private int mConsecutiveUpdates; + // The last adapter that bound this ViewHolder. It is cleaned before VH is recycled. + Adapter mBindingAdapter; - /** - * @param dx Pixels to scroll horizontally - * @param dy Pixels to scroll vertically - */ - public Action(@Px int dx, @Px int dy) { - this(dx, dy, UNDEFINED_DURATION, null); + public ViewHolder(@NonNull View itemView) { + if (itemView == null) { + throw new IllegalArgumentException("itemView may not be null"); } + this.itemView = itemView; + } - /** - * @param dx Pixels to scroll horizontally - * @param dy Pixels to scroll vertically - * @param duration Duration of the animation in milliseconds - */ - public Action(@Px int dx, @Px int dy, int duration) { - this(dx, dy, duration, null); - } + void flagRemovedAndOffsetPosition(int mNewPosition, int offset, boolean applyToPreLayout) { + addFlags(ViewHolder.FLAG_REMOVED); + offsetPosition(offset, applyToPreLayout); + mPosition = mNewPosition; + } - /** - * @param dx Pixels to scroll horizontally - * @param dy Pixels to scroll vertically - * @param duration Duration of the animation in milliseconds - * @param interpolator Interpolator to be used when calculating scroll position in each - * animation step - */ - public Action(@Px int dx, @Px int dy, int duration, - @Nullable Interpolator interpolator) { - mDx = dx; - mDy = dy; - mDuration = duration; - mInterpolator = interpolator; + void offsetPosition(int offset, boolean applyToPreLayout) { + if (mOldPosition == NO_POSITION) { + mOldPosition = mPosition; } - - /** - * Instead of specifying pixels to scroll, use the target position to jump using - * {@link RecyclerView#scrollToPosition(int)}. - *

    - * You may prefer using this method if scroll target is really far away and you prefer - * to jump to a location and smooth scroll afterwards. - *

    - * Note that calling this method takes priority over other update methods such as - * {@link #update(int, int, int, Interpolator)}, {@link #setX(float)}, - * {@link #setY(float)} and #{@link #setInterpolator(Interpolator)}. If you call - * {@link #jumpTo(int)}, the other changes will not be considered for this animation - * frame. - * - * @param targetPosition The target item position to scroll to using instant scrolling. - */ - public void jumpTo(int targetPosition) { - mJumpToPosition = targetPosition; + if (mPreLayoutPosition == NO_POSITION) { + mPreLayoutPosition = mPosition; } - - boolean hasJumpTarget() { - return mJumpToPosition >= 0; + if (applyToPreLayout) { + mPreLayoutPosition += offset; } - - void runIfNecessary(RecyclerView recyclerView) { - if (mJumpToPosition >= 0) { - int position = mJumpToPosition; - mJumpToPosition = NO_POSITION; - recyclerView.jumpToPositionForSmoothScroller(position); - mChanged = false; - return; - } - if (mChanged) { - validate(); - recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration, mInterpolator); - mConsecutiveUpdates++; - if (mConsecutiveUpdates > 10) { - // A new action is being set in every animation step. This looks like a bad - // implementation. Inform developer. - Log.e(TAG, "Smooth Scroll action is being updated too frequently. Make sure" - + " you are not changing it unless necessary"); - } - mChanged = false; - } else { - mConsecutiveUpdates = 0; - } + mPosition += offset; + if (itemView.getLayoutParams() != null) { + ((LayoutParams) itemView.getLayoutParams()).mInsetsDirty = true; } + } - private void validate() { - if (mInterpolator != null && mDuration < 1) { - throw new IllegalStateException("If you provide an interpolator, you must" - + " set a positive duration"); - } else if (mDuration < 1) { - throw new IllegalStateException("Scroll duration must be a positive number"); - } - } + void clearOldPosition() { + mOldPosition = NO_POSITION; + mPreLayoutPosition = NO_POSITION; + } - @Px - public int getDx() { - return mDx; + void saveOldPosition() { + if (mOldPosition == NO_POSITION) { + mOldPosition = mPosition; } + } - public void setDx(@Px int dx) { - mChanged = true; - mDx = dx; - } + boolean shouldIgnore() { + return (mFlags & FLAG_IGNORE) != 0; + } - @Px - public int getDy() { - return mDy; - } + /** + * @see #getLayoutPosition() + * @see #getBindingAdapterPosition() + * @see #getAbsoluteAdapterPosition() + * @deprecated This method is deprecated because its meaning is ambiguous due to the async + * handling of adapter updates. You should use {@link #getLayoutPosition()}, + * {@link #getBindingAdapterPosition()} or {@link #getAbsoluteAdapterPosition()} + * depending on your use case. + */ + @Deprecated + public final int getPosition() { + return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition; + } - public void setDy(@Px int dy) { - mChanged = true; - mDy = dy; - } + /** + * Returns the position of the ViewHolder in terms of the latest layout pass. + *

    + * This position is mostly used by RecyclerView components to be consistent while + * RecyclerView lazily processes adapter updates. + *

    + * For performance and animation reasons, RecyclerView batches all adapter updates until the + * next layout pass. This may cause mismatches between the Adapter position of the item and + * the position it had in the latest layout calculations. + *

    + * LayoutManagers should always call this method while doing calculations based on item + * positions. All methods in {@link RecyclerView.LayoutManager}, {@link RecyclerView.State}, + * {@link RecyclerView.Recycler} that receive a position expect it to be the layout position + * of the item. + *

    + * If LayoutManager needs to call an external method that requires the adapter position of + * the item, it can use {@link #getAbsoluteAdapterPosition()} or + * {@link RecyclerView.Recycler#convertPreLayoutPositionToPostLayout(int)}. + * + * @return Returns the adapter position of the ViewHolder in the latest layout pass. + * @see #getBindingAdapterPosition() + * @see #getAbsoluteAdapterPosition() + */ + public final int getLayoutPosition() { + return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition; + } - public int getDuration() { - return mDuration; - } - public void setDuration(int duration) { - mChanged = true; - mDuration = duration; - } + /** + * @return {@link #getBindingAdapterPosition()} + * @deprecated This method is confusing when adapters nest other adapters. + * If you are calling this in the context of an Adapter, you probably want to call + * {@link #getBindingAdapterPosition()} or if you want the position as {@link RecyclerView} + * sees it, you should call {@link #getAbsoluteAdapterPosition()}. + */ + @Deprecated + public final int getAdapterPosition() { + return getBindingAdapterPosition(); + } - @Nullable - public Interpolator getInterpolator() { - return mInterpolator; + /** + * Returns the Adapter position of the item represented by this ViewHolder with respect to + * the {@link Adapter} that bound it. + *

    + * Note that this might be different than the {@link #getLayoutPosition()} if there are + * pending adapter updates but a new layout pass has not happened yet. + *

    + * RecyclerView does not handle any adapter updates until the next layout traversal. This + * may create temporary inconsistencies between what user sees on the screen and what + * adapter contents have. This inconsistency is not important since it will be less than + * 16ms but it might be a problem if you want to use ViewHolder position to access the + * adapter. Sometimes, you may need to get the exact adapter position to do + * some actions in response to user events. In that case, you should use this method which + * will calculate the Adapter position of the ViewHolder. + *

    + * Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the + * next layout pass, the return value of this method will be {@link #NO_POSITION}. + *

    + * If the {@link Adapter} that bound this {@link ViewHolder} is inside another + * {@link Adapter} (e.g. {@link ConcatAdapter}), this position might be different than + * {@link #getAbsoluteAdapterPosition()}. If you would like to know the position that + * {@link RecyclerView} considers (e.g. for saved state), you should use + * {@link #getAbsoluteAdapterPosition()}. + * + * @return The adapter position of the item if it still exists in the adapter. + * {@link RecyclerView#NO_POSITION} if item has been removed from the adapter, + * {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last + * layout pass or the ViewHolder has already been recycled. + * @see #getAbsoluteAdapterPosition() + * @see #getLayoutPosition() + */ + public final int getBindingAdapterPosition() { + if (mBindingAdapter == null) { + return NO_POSITION; } - - /** - * Sets the interpolator to calculate scroll steps - * - * @param interpolator The interpolator to use. If you specify an interpolator, you must - * also set the duration. - * @see #setDuration(int) - */ - public void setInterpolator(@Nullable Interpolator interpolator) { - mChanged = true; - mInterpolator = interpolator; + if (mOwnerRecyclerView == null) { + return NO_POSITION; } - - /** - * Updates the action with given parameters. - * - * @param dx Pixels to scroll horizontally - * @param dy Pixels to scroll vertically - * @param duration Duration of the animation in milliseconds - * @param interpolator Interpolator to be used when calculating scroll position in each - * animation step - */ - public void update(@Px int dx, @Px int dy, int duration, - @Nullable Interpolator interpolator) { - mDx = dx; - mDy = dy; - mDuration = duration; - mInterpolator = interpolator; - mChanged = true; + @SuppressWarnings("unchecked") + Adapter rvAdapter = mOwnerRecyclerView.getAdapter(); + if (rvAdapter == null) { + return NO_POSITION; } + int globalPosition = mOwnerRecyclerView.getAdapterPositionInRecyclerView(this); + if (globalPosition == NO_POSITION) { + return NO_POSITION; + } + return rvAdapter.findRelativeAdapterPositionIn(mBindingAdapter, this, globalPosition); } - } - static class AdapterDataObservable extends Observable { - public boolean hasObservers() { - return !mObservers.isEmpty(); + /** + * Returns the Adapter position of the item represented by this ViewHolder with respect to + * the {@link RecyclerView}'s {@link Adapter}. If the {@link Adapter} that bound this + * {@link ViewHolder} is inside another adapter (e.g. {@link ConcatAdapter}), this + * position might be different and will include + * the offsets caused by other adapters in the {@link ConcatAdapter}. + *

    + * Note that this might be different than the {@link #getLayoutPosition()} if there are + * pending adapter updates but a new layout pass has not happened yet. + *

    + * RecyclerView does not handle any adapter updates until the next layout traversal. This + * may create temporary inconsistencies between what user sees on the screen and what + * adapter contents have. This inconsistency is not important since it will be less than + * 16ms but it might be a problem if you want to use ViewHolder position to access the + * adapter. Sometimes, you may need to get the exact adapter position to do + * some actions in response to user events. In that case, you should use this method which + * will calculate the Adapter position of the ViewHolder. + *

    + * Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the + * next layout pass, the return value of this method will be {@link #NO_POSITION}. + *

    + * Note that if you are querying the position as {@link RecyclerView} sees, you should use + * {@link #getAbsoluteAdapterPosition()} (e.g. you want to use it to save scroll + * state). If you are querying the position to access the {@link Adapter} contents, + * you should use {@link #getBindingAdapterPosition()}. + * + * @return The adapter position of the item from {@link RecyclerView}'s perspective if it + * still exists in the adapter and bound to a valid item. + * {@link RecyclerView#NO_POSITION} if item has been removed from the adapter, + * {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last + * layout pass or the ViewHolder has already been recycled. + * @see #getBindingAdapterPosition() + * @see #getLayoutPosition() + */ + public final int getAbsoluteAdapterPosition() { + if (mOwnerRecyclerView == null) { + return NO_POSITION; + } + return mOwnerRecyclerView.getAdapterPositionInRecyclerView(this); } - public void notifyChanged() { - // since onChanged() is implemented by the app, it could do anything, including - // removing itself from {@link mObservers} - and that could cause problems if - // an iterator is used on the ArrayList {@link mObservers}. - // to avoid such problems, just march thru the list in the reverse order. - for (int i = mObservers.size() - 1; i >= 0; i--) { - mObservers.get(i).onChanged(); - } + /** + * Returns the {@link Adapter} that last bound this {@link ViewHolder}. + * Might return {@code null} if this {@link ViewHolder} is not bound to any adapter. + * + * @return The {@link Adapter} that last bound this {@link ViewHolder} or {@code null} if + * this {@link ViewHolder} is not bound by any adapter (e.g. recycled). + */ + @Nullable + public final Adapter getBindingAdapter() { + return mBindingAdapter; } - public void notifyStateRestorationPolicyChanged() { - for (int i = mObservers.size() - 1; i >= 0; i--) { - mObservers.get(i).onStateRestorationPolicyChanged(); - } + /** + * When LayoutManager supports animations, RecyclerView tracks 3 positions for ViewHolders + * to perform animations. + *

    + * If a ViewHolder was laid out in the previous onLayout call, old position will keep its + * adapter index in the previous layout. + * + * @return The previous adapter index of the Item represented by this ViewHolder or + * {@link #NO_POSITION} if old position does not exists or cleared (pre-layout is + * complete). + */ + public final int getOldPosition() { + return mOldPosition; } - public void notifyItemRangeChanged(int positionStart, int itemCount) { - notifyItemRangeChanged(positionStart, itemCount, null); + /** + * Returns The itemId represented by this ViewHolder. + * + * @return The item's id if adapter has stable ids, {@link RecyclerView#NO_ID} + * otherwise + */ + public final long getItemId() { + return mItemId; } - public void notifyItemRangeChanged(int positionStart, int itemCount, - @Nullable Object payload) { - // since onItemRangeChanged() is implemented by the app, it could do anything, including - // removing itself from {@link mObservers} - and that could cause problems if - // an iterator is used on the ArrayList {@link mObservers}. - // to avoid such problems, just march thru the list in the reverse order. - for (int i = mObservers.size() - 1; i >= 0; i--) { - mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload); - } + /** + * @return The view type of this ViewHolder. + */ + public final int getItemViewType() { + return mItemViewType; } - public void notifyItemRangeInserted(int positionStart, int itemCount) { - // since onItemRangeInserted() is implemented by the app, it could do anything, - // including removing itself from {@link mObservers} - and that could cause problems if - // an iterator is used on the ArrayList {@link mObservers}. - // to avoid such problems, just march thru the list in the reverse order. - for (int i = mObservers.size() - 1; i >= 0; i--) { - mObservers.get(i).onItemRangeInserted(positionStart, itemCount); - } + boolean isScrap() { + return mScrapContainer != null; } - public void notifyItemRangeRemoved(int positionStart, int itemCount) { - // since onItemRangeRemoved() is implemented by the app, it could do anything, including - // removing itself from {@link mObservers} - and that could cause problems if - // an iterator is used on the ArrayList {@link mObservers}. - // to avoid such problems, just march thru the list in the reverse order. - for (int i = mObservers.size() - 1; i >= 0; i--) { - mObservers.get(i).onItemRangeRemoved(positionStart, itemCount); - } + void unScrap() { + mScrapContainer.unscrapView(this); } - public void notifyItemMoved(int fromPosition, int toPosition) { - for (int i = mObservers.size() - 1; i >= 0; i--) { - mObservers.get(i).onItemRangeMoved(fromPosition, toPosition, 1); - } + boolean wasReturnedFromScrap() { + return (mFlags & FLAG_RETURNED_FROM_SCRAP) != 0; } - } - /** - * This is public so that the CREATOR can be accessed on cold launch. - * - * @hide - */ - @RestrictTo(LIBRARY) - public static class SavedState extends AbsSavedState { + void clearReturnedFromScrapFlag() { + mFlags = mFlags & ~FLAG_RETURNED_FROM_SCRAP; + } - public static final Creator CREATOR = new ClassLoaderCreator() { - @Override - public SavedState createFromParcel(Parcel in, ClassLoader loader) { - return new SavedState(in, loader); - } + void clearTmpDetachFlag() { + mFlags = mFlags & ~FLAG_TMP_DETACHED; + } - @Override - public SavedState createFromParcel(Parcel in) { - return new SavedState(in, null); - } + void stopIgnoring() { + mFlags = mFlags & ~FLAG_IGNORE; + } - @Override - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - Parcelable mLayoutState; + void setScrapContainer(Recycler recycler, boolean isChangeScrap) { + mScrapContainer = recycler; + mInChangeScrap = isChangeScrap; + } - /** - * called by CREATOR - */ - @SuppressWarnings("deprecation") - SavedState(Parcel in, ClassLoader loader) { - super(in, loader); - mLayoutState = in.readParcelable( - loader != null ? loader : LayoutManager.class.getClassLoader()); + boolean isInvalid() { + return (mFlags & FLAG_INVALID) != 0; } - /** - * Called by onSaveInstanceState - */ - SavedState(Parcelable superState) { - super(superState); + boolean needsUpdate() { + return (mFlags & FLAG_UPDATE) != 0; } - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeParcelable(mLayoutState, 0); + boolean isBound() { + return (mFlags & FLAG_BOUND) != 0; } - void copyFrom(SavedState other) { - mLayoutState = other.mLayoutState; + boolean isRemoved() { + return (mFlags & FLAG_REMOVED) != 0; } - } - /** - *

    Contains useful information about the current RecyclerView state like target scroll - * position or view focus. State object can also keep arbitrary data, identified by resource - * ids.

    - *

    Often times, RecyclerView components will need to pass information between each other. - * To provide a well defined data bus between components, RecyclerView passes the same State - * object to component callbacks and these components can use it to exchange data.

    - *

    If you implement custom components, you can use State's put/get/remove methods to pass - * data between your components without needing to manage their lifecycles.

    - */ - public static class State { - static final int STEP_START = 1; - static final int STEP_LAYOUT = 1 << 1; - static final int STEP_ANIMATIONS = 1 << 2; - /** - * Owned by SmoothScroller - */ - int mTargetPosition = NO_POSITION; - /** - * Number of items adapter had in the previous layout. - */ - int mPreviousLayoutItemCount; - /** - * Number of items that were NOT laid out but has been deleted from the adapter after the - * previous layout. - */ - int mDeletedInvisibleItemCountSincePreviousLayout; + boolean hasAnyOfTheFlags(int flags) { + return (mFlags & flags) != 0; + } - //////////////////////////////////////////////////////////////////////////////////////////// - // Fields below are carried from one layout pass to the next - //////////////////////////////////////////////////////////////////////////////////////////// - @LayoutState - int mLayoutStep = STEP_START; - /** - * Number of items adapter has. - */ - int mItemCount; + boolean isTmpDetached() { + return (mFlags & FLAG_TMP_DETACHED) != 0; + } - //////////////////////////////////////////////////////////////////////////////////////////// - // Fields below must be updated or cleared before they are used (generally before a pass) - //////////////////////////////////////////////////////////////////////////////////////////// - boolean mStructureChanged; - /** - * True if the associated {@link RecyclerView} is in the pre-layout step where it is having - * its {@link LayoutManager} layout items where they will be at the beginning of a set of - * predictive item animations. - */ - boolean mInPreLayout; - boolean mTrackOldChangeHolders; - boolean mIsMeasuring; - boolean mRunSimpleAnimations; - boolean mRunPredictiveAnimations; - /** - * This data is saved before a layout calculation happens. After the layout is finished, - * if the previously focused view has been replaced with another view for the same item, we - * move the focus to the new item automatically. - */ - int mFocusedItemPosition; + boolean isAttachedToTransitionOverlay() { + return itemView.getParent() != null && itemView.getParent() != mOwnerRecyclerView; + } - //////////////////////////////////////////////////////////////////////////////////////////// - // Fields below are always reset outside of the pass (or passes) that use them - //////////////////////////////////////////////////////////////////////////////////////////// - long mFocusedItemId; - // when a sub child has focus, record its id and see if we can directly request focus on - // that one instead - int mFocusedSubChildId; - int mRemainingScrollHorizontal; - int mRemainingScrollVertical; - private SparseArray mData; + boolean isAdapterPositionUnknown() { + return (mFlags & FLAG_ADAPTER_POSITION_UNKNOWN) != 0 || isInvalid(); + } - void assertLayoutStep(int accepted) { - if ((accepted & mLayoutStep) == 0) { - throw new IllegalStateException("Layout state should be one of " - + Integer.toBinaryString(accepted) + " but it is " - + Integer.toBinaryString(mLayoutStep)); - } + void setFlags(int flags, int mask) { + mFlags = (mFlags & ~mask) | (flags & mask); } - /** - * Prepare for a prefetch occurring on the RecyclerView in between traversals, potentially - * prior to any layout passes. - * - *

    Don't touch any state stored between layout passes, only reset per-layout state, so - * that Recycler#getViewForPosition() can function safely.

    - */ - void prepareForNestedPrefetch(Adapter adapter) { - mLayoutStep = STEP_START; - mItemCount = adapter.getItemCount(); - mInPreLayout = false; - mTrackOldChangeHolders = false; - mIsMeasuring = false; + void addFlags(int flags) { + mFlags |= flags; } - //////////////////////////////////////////////////////////////////////////////////////////// + void addChangePayload(Object payload) { + if (payload == null) { + addFlags(FLAG_ADAPTER_FULLUPDATE); + } else if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) { + createPayloadsIfNeeded(); + mPayloads.add(payload); + } + } - /** - * Returns true if the RecyclerView is currently measuring the layout. This value is - * {@code true} only if the LayoutManager opted into the auto measure API and RecyclerView - * has non-exact measurement specs. - *

    - * Note that if the LayoutManager supports predictive animations and it is calculating the - * pre-layout step, this value will be {@code false} even if the RecyclerView is in - * {@code onMeasure} call. This is because pre-layout means the previous state of the - * RecyclerView and measurements made for that state cannot change the RecyclerView's size. - * LayoutManager is always guaranteed to receive another call to - * {@link LayoutManager#onLayoutChildren(Recycler, State)} when this happens. - * - * @return True if the RecyclerView is currently calculating its bounds, false otherwise. - */ - public boolean isMeasuring() { - return mIsMeasuring; + private void createPayloadsIfNeeded() { + if (mPayloads == null) { + mPayloads = new ArrayList<>(); + mUnmodifiedPayloads = Collections.unmodifiableList(mPayloads); + } } - /** - * Returns true if the {@link RecyclerView} is in the pre-layout step where it is having its - * {@link LayoutManager} layout items where they will be at the beginning of a set of - * predictive item animations. - */ - public boolean isPreLayout() { - return mInPreLayout; + void clearPayload() { + if (mPayloads != null) { + mPayloads.clear(); + } + mFlags = mFlags & ~FLAG_ADAPTER_FULLUPDATE; } - /** - * Returns whether RecyclerView will run predictive animations in this layout pass - * or not. - * - * @return true if RecyclerView is calculating predictive animations to be run at the end - * of the layout pass. - */ - public boolean willRunPredictiveAnimations() { - return mRunPredictiveAnimations; + List getUnmodifiedPayloads() { + if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) { + if (mPayloads == null || mPayloads.size() == 0) { + // Initial state, no update being called. + return FULLUPDATE_PAYLOADS; + } + // there are none-null payloads + return mUnmodifiedPayloads; + } else { + // a full update has been called. + return FULLUPDATE_PAYLOADS; + } } - /** - * Returns whether RecyclerView will run simple animations in this layout pass - * or not. - * - * @return true if RecyclerView is calculating simple animations to be run at the end of - * the layout pass. - */ - public boolean willRunSimpleAnimations() { - return mRunSimpleAnimations; + void resetInternal() { + mFlags = 0; + mPosition = NO_POSITION; + mOldPosition = NO_POSITION; + mItemId = NO_ID; + mPreLayoutPosition = NO_POSITION; + mIsRecyclableCount = 0; + mShadowedHolder = null; + mShadowingHolder = null; + clearPayload(); + mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET; + clearNestedRecyclerViewIfNotNested(this); } /** - * Removes the mapping from the specified id, if there was any. - * - * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.* to - * preserve cross functionality and avoid conflicts. + * Called when the child view enters the hidden state */ - public void remove(int resourceId) { - if (mData == null) { - return; + void onEnteredHiddenState(RecyclerView parent) { + // While the view item is in hidden state, make it invisible for the accessibility. + if (mPendingAccessibilityState != PENDING_ACCESSIBILITY_STATE_NOT_SET) { + mWasImportantForAccessibilityBeforeHidden = mPendingAccessibilityState; + } else { + mWasImportantForAccessibilityBeforeHidden = + ViewCompat.getImportantForAccessibility(itemView); } - mData.remove(resourceId); + parent.setChildImportantForAccessibilityInternal(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); } /** - * Gets the Object mapped from the specified id, or null - * if no such data exists. - * - * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.* - * to - * preserve cross functionality and avoid conflicts. + * Called when the child view leaves the hidden state */ - @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"}) - public T get(int resourceId) { - if (mData == null) { - return null; + void onLeftHiddenState(RecyclerView parent) { + parent.setChildImportantForAccessibilityInternal(this, + mWasImportantForAccessibilityBeforeHidden); + mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + } + + @Override + public String toString() { + String className = + getClass().isAnonymousClass() ? "ViewHolder" : getClass().getSimpleName(); + final StringBuilder sb = new StringBuilder(className + "{" + + Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId + + ", oldPos=" + mOldPosition + ", pLpos:" + mPreLayoutPosition); + if (isScrap()) { + sb.append(" scrap ") + .append(mInChangeScrap ? "[changeScrap]" : "[attachedScrap]"); } - return (T) mData.get(resourceId); + if (isInvalid()) sb.append(" invalid"); + if (!isBound()) sb.append(" unbound"); + if (needsUpdate()) sb.append(" update"); + if (isRemoved()) sb.append(" removed"); + if (shouldIgnore()) sb.append(" ignored"); + if (isTmpDetached()) sb.append(" tmpDetached"); + if (!isRecyclable()) sb.append(" not recyclable(").append(mIsRecyclableCount).append(")"); + if (isAdapterPositionUnknown()) sb.append(" undefined adapter position"); + + if (itemView.getParent() == null) sb.append(" no parent"); + sb.append("}"); + return sb.toString(); } /** - * Adds a mapping from the specified id to the specified value, replacing the previous - * mapping from the specified key if there was one. + * Informs the recycler whether this item can be recycled. Views which are not + * recyclable will not be reused for other items until setIsRecyclable() is + * later set to true. Calls to setIsRecyclable() should always be paired (one + * call to setIsRecyclabe(false) should always be matched with a later call to + * setIsRecyclable(true)). Pairs of calls may be nested, as the state is internally + * reference-counted. * - * @param resourceId Id of the resource you want to add. It is suggested to use R.id.* to - * preserve cross functionality and avoid conflicts. - * @param data The data you want to associate with the resourceId. + * @param recyclable Whether this item is available to be recycled. Default value + * is true. + * @see #isRecyclable() */ - public void put(int resourceId, Object data) { - if (mData == null) { - mData = new SparseArray<>(); + public final void setIsRecyclable(boolean recyclable) { + mIsRecyclableCount = recyclable ? mIsRecyclableCount - 1 : mIsRecyclableCount + 1; + if (mIsRecyclableCount < 0) { + mIsRecyclableCount = 0; + if (DEBUG) { + throw new RuntimeException("isRecyclable decremented below 0: " + + "unmatched pair of setIsRecyable() calls for " + this); + } + Log.e(VIEW_LOG_TAG, "isRecyclable decremented below 0: " + + "unmatched pair of setIsRecyable() calls for " + this); + } else if (!recyclable && mIsRecyclableCount == 1) { + mFlags |= FLAG_NOT_RECYCLABLE; + } else if (recyclable && mIsRecyclableCount == 0) { + mFlags &= ~FLAG_NOT_RECYCLABLE; + } + if (DEBUG) { + Log.d(TAG, "setIsRecyclable val:" + recyclable + ":" + this); } - mData.put(resourceId, data); } /** - * If scroll is triggered to make a certain item visible, this value will return the - * adapter index of that item. - * - * @return Adapter index of the target item or - * {@link RecyclerView#NO_POSITION} if there is no target - * position. + * @return true if this item is available to be recycled, false otherwise. + * @see #setIsRecyclable(boolean) */ - public int getTargetScrollPosition() { - return mTargetPosition; + public final boolean isRecyclable() { + return (mFlags & FLAG_NOT_RECYCLABLE) == 0 + && !ViewCompat.hasTransientState(itemView); } /** - * Returns if current scroll has a target position. - * - * @return true if scroll is being triggered to make a certain position visible - * @see #getTargetScrollPosition() + * Returns whether we have animations referring to this view holder or not. + * This is similar to isRecyclable flag but does not check transient state. */ - public boolean hasTargetScrollPosition() { - return mTargetPosition != NO_POSITION; + boolean shouldBeKeptAsChild() { + return (mFlags & FLAG_NOT_RECYCLABLE) != 0; } /** - * @return true if the structure of the data set has changed since the last call to - * onLayoutChildren, false otherwise + * @return True if ViewHolder is not referenced by RecyclerView animations but has + * transient state which will prevent it from being recycled. */ - public boolean didStructureChange() { - return mStructureChanged; + boolean doesTransientStatePreventRecycling() { + return (mFlags & FLAG_NOT_RECYCLABLE) == 0 && ViewCompat.hasTransientState(itemView); } - /** - * Returns the total number of items that can be laid out. Note that this number is not - * necessarily equal to the number of items in the adapter, so you should always use this - * number for your position calculations and never access the adapter directly. - *

    - * RecyclerView listens for Adapter's notify events and calculates the effects of adapter - * data changes on existing Views. These calculations are used to decide which animations - * should be run. - *

    - * To support predictive animations, RecyclerView may rewrite or reorder Adapter changes to - * present the correct state to LayoutManager in pre-layout pass. - *

    - * For example, a newly added item is not included in pre-layout item count because - * pre-layout reflects the contents of the adapter before the item is added. Behind the - * scenes, RecyclerView offsets {@link Recycler#getViewForPosition(int)} calls such that - * LayoutManager does not know about the new item's existence in pre-layout. The item will - * be available in second layout pass and will be included in the item count. Similar - * adjustments are made for moved and removed items as well. - *

    - * You can get the adapter's item count via {@link LayoutManager#getItemCount()} method. - * - * @return The number of items currently available - * @see LayoutManager#getItemCount() - */ - public int getItemCount() { - return mInPreLayout - ? (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout) - : mItemCount; + boolean isUpdated() { + return (mFlags & FLAG_UPDATE) != 0; } + } - /** - * Returns remaining horizontal scroll distance of an ongoing scroll animation(fling/ - * smoothScrollTo/SmoothScroller) in pixels. Returns zero if {@link #getScrollState()} is - * other than {@link #SCROLL_STATE_SETTLING}. - * - * @return Remaining horizontal scroll distance - */ - public int getRemainingScrollHorizontal() { - return mRemainingScrollHorizontal; + /** + * This method is here so that we can control the important for a11y changes and test it. + */ + @VisibleForTesting + boolean setChildImportantForAccessibilityInternal(ViewHolder viewHolder, + int importantForAccessibility) { + if (isComputingLayout()) { + viewHolder.mPendingAccessibilityState = importantForAccessibility; + mPendingAccessibilityImportanceChange.add(viewHolder); + return false; } + ViewCompat.setImportantForAccessibility(viewHolder.itemView, importantForAccessibility); + return true; + } - /** - * Returns remaining vertical scroll distance of an ongoing scroll animation(fling/ - * smoothScrollTo/SmoothScroller) in pixels. Returns zero if {@link #getScrollState()} is - * other than {@link #SCROLL_STATE_SETTLING}. - * - * @return Remaining vertical scroll distance - */ - public int getRemainingScrollVertical() { - return mRemainingScrollVertical; + void dispatchPendingImportantForAccessibilityChanges() { + for (int i = mPendingAccessibilityImportanceChange.size() - 1; i >= 0; i--) { + ViewHolder viewHolder = mPendingAccessibilityImportanceChange.get(i); + if (viewHolder.itemView.getParent() != this || viewHolder.shouldIgnore()) { + continue; + } + int state = viewHolder.mPendingAccessibilityState; + if (state != ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET) { + //noinspection WrongConstant + ViewCompat.setImportantForAccessibility(viewHolder.itemView, state); + viewHolder.mPendingAccessibilityState = + ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET; + } } + mPendingAccessibilityImportanceChange.clear(); + } - @NonNull - @Override - public String toString() { - return "State{" - + "mTargetPosition=" + mTargetPosition - + ", mData=" + mData - + ", mItemCount=" + mItemCount - + ", mIsMeasuring=" + mIsMeasuring - + ", mPreviousLayoutItemCount=" + mPreviousLayoutItemCount - + ", mDeletedInvisibleItemCountSincePreviousLayout=" - + mDeletedInvisibleItemCountSincePreviousLayout - + ", mStructureChanged=" + mStructureChanged - + ", mInPreLayout=" + mInPreLayout - + ", mRunSimpleAnimations=" + mRunSimpleAnimations - + ", mRunPredictiveAnimations=" + mRunPredictiveAnimations - + '}'; + int getAdapterPositionInRecyclerView(ViewHolder viewHolder) { + if (viewHolder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID + | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN) + || !viewHolder.isBound()) { + return RecyclerView.NO_POSITION; } + return mAdapterHelper.applyPendingUpdatesToPosition(viewHolder.mPosition); + } - @IntDef(flag = true, value = { - STEP_START, STEP_LAYOUT, STEP_ANIMATIONS - }) - @Retention(RetentionPolicy.SOURCE) - @interface LayoutState { + @VisibleForTesting + void initFastScroller(StateListDrawable verticalThumbDrawable, + Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable, + Drawable horizontalTrackDrawable) { + if (verticalThumbDrawable == null || verticalTrackDrawable == null + || horizontalThumbDrawable == null || horizontalTrackDrawable == null) { + throw new IllegalArgumentException( + "Trying to set fast scroller without both required drawables." + + exceptionLabel()); } + + Resources resources = getContext().getResources(); + new FastScroller(this, verticalThumbDrawable, verticalTrackDrawable, + horizontalThumbDrawable, horizontalTrackDrawable, + resources.getDimensionPixelSize(R.dimen.fastscroll_default_thickness), + resources.getDimensionPixelSize(R.dimen.fastscroll_minimum_range), + resources.getDimensionPixelOffset(R.dimen.fastscroll_margin)); } - /** - * This class defines the behavior of fling if the developer wishes to handle it. - *

    - * Subclasses of {@link OnFlingListener} can be used to implement custom fling behavior. - * - * @see #setOnFlingListener(OnFlingListener) - */ - public abstract static class OnFlingListener { + // NestedScrollingChild - /** - * Override this to handle a fling given the velocities in both x and y directions. - * Note that this method will only be called if the associated {@link LayoutManager} - * supports scrolling and the fling is not handled by nested scrolls first. - * - * @param velocityX the fling velocity on the X axis - * @param velocityY the fling velocity on the Y axis - * @return true if the fling was handled, false otherwise. - */ - public abstract boolean onFling(int velocityX, int velocityY); + @Override + public void setNestedScrollingEnabled(boolean enabled) { + getScrollingChildHelper().setNestedScrollingEnabled(enabled); } - /** - * This class defines the animations that take place on items as changes are made - * to the adapter. - *

    - * Subclasses of ItemAnimator can be used to implement custom animations for actions on - * ViewHolder items. The RecyclerView will manage retaining these items while they - * are being animated, but implementors must call {@link #dispatchAnimationFinished(ViewHolder)} - * when a ViewHolder's animation is finished. In other words, there must be a matching - * {@link #dispatchAnimationFinished(ViewHolder)} call for each - * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) animateAppearance()}, - * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateChange()} - * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) animatePersistence()}, - * and - * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateDisappearance()} call. - * - *

    By default, RecyclerView uses {@link DefaultItemAnimator}.

    - * - * @see #setItemAnimator(ItemAnimator) - */ - @SuppressWarnings("UnusedParameters") - public abstract static class ItemAnimator { + @Override + public boolean isNestedScrollingEnabled() { + return getScrollingChildHelper().isNestedScrollingEnabled(); + } + + @Override + public boolean startNestedScroll(int axes) { + return getScrollingChildHelper().startNestedScroll(axes); + } + + @Override + public boolean startNestedScroll(int axes, int type) { + return getScrollingChildHelper().startNestedScroll(axes, type); + } + + @Override + public void stopNestedScroll() { + getScrollingChildHelper().stopNestedScroll(); + } + + @Override + public void stopNestedScroll(int type) { + getScrollingChildHelper().stopNestedScroll(type); + } + + @Override + public boolean hasNestedScrollingParent() { + return getScrollingChildHelper().hasNestedScrollingParent(); + } - /** - * The Item represented by this ViewHolder is updated. - *

    - * - * @see #recordPreLayoutInformation(State, ViewHolder, int, List) - */ - public static final int FLAG_CHANGED = ViewHolder.FLAG_UPDATE; + @Override + public boolean hasNestedScrollingParent(int type) { + return getScrollingChildHelper().hasNestedScrollingParent(type); + } - /** - * The Item represented by this ViewHolder is removed from the adapter. - *

    - * - * @see #recordPreLayoutInformation(State, ViewHolder, int, List) - */ - public static final int FLAG_REMOVED = ViewHolder.FLAG_REMOVED; + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow) { + return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, offsetInWindow); + } - /** - * Adapter {@link Adapter#notifyDataSetChanged()} has been called and the content - * represented by this ViewHolder is invalid. - *

    - * - * @see #recordPreLayoutInformation(State, ViewHolder, int, List) - */ - public static final int FLAG_INVALIDATED = ViewHolder.FLAG_INVALID; + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow, int type) { + return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, offsetInWindow, type); + } - /** - * The position of the Item represented by this ViewHolder has been changed. This flag is - * not bound to {@link Adapter#notifyItemMoved(int, int)}. It might be set in response to - * any adapter change that may have a side effect on this item. (e.g. The item before this - * one has been removed from the Adapter). - *

    - * - * @see #recordPreLayoutInformation(State, ViewHolder, int, List) - */ - public static final int FLAG_MOVED = ViewHolder.FLAG_MOVED; + @Override + public final void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow, int type, @NonNull int[] consumed) { + getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed); + } - /** - * This ViewHolder was not laid out but has been added to the layout in pre-layout state - * by the {@link LayoutManager}. This means that the item was already in the Adapter but - * invisible and it may become visible in the post layout phase. LayoutManagers may prefer - * to add new items in pre-layout to specify their virtual location when they are invisible - * (e.g. to specify the item should animate in from below the visible area). - *

    - * - * @see #recordPreLayoutInformation(State, ViewHolder, int, List) - */ - public static final int FLAG_APPEARED_IN_PRE_LAYOUT = - ViewHolder.FLAG_APPEARED_IN_PRE_LAYOUT; - private final ArrayList mFinishedListeners = - new ArrayList<>(); - private ItemAnimatorListener mListener; - private long mAddDuration = 120; - private long mRemoveDuration = 120; - private long mMoveDuration = 250; - private long mChangeDuration = 250; + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } - @AdapterChanges - static int buildAdapterChangeFlagsForAnimations(ViewHolder viewHolder) { - int flags = viewHolder.mFlags & (FLAG_INVALIDATED | FLAG_REMOVED | FLAG_CHANGED); - if (viewHolder.isInvalid()) { - return FLAG_INVALIDATED; - } - if ((flags & FLAG_INVALIDATED) == 0) { - int oldPos = viewHolder.getOldPosition(); - int pos = viewHolder.getAbsoluteAdapterPosition(); - if (oldPos != NO_POSITION && pos != NO_POSITION && oldPos != pos) { - flags |= FLAG_MOVED; - } - } - return flags; - } + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, + int type) { + return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, + type); + } - /** - * Gets the current duration for which all move animations will run. - * - * @return The current move duration - */ - public long getMoveDuration() { - return mMoveDuration; + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY); + } + + /** + * {@link android.view.ViewGroup.MarginLayoutParams LayoutParams} subclass for children of + * {@link RecyclerView}. Custom {@link LayoutManager layout managers} are encouraged + * to create their own subclass of this LayoutParams class + * to store any additional required per-child view metadata about the layout. + */ + public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams { + ViewHolder mViewHolder; + final Rect mDecorInsets = new Rect(); + boolean mInsetsDirty = true; + // Flag is set to true if the view is bound while it is detached from RV. + // In this case, we need to manually call invalidate after view is added to guarantee that + // invalidation is populated through the View hierarchy + boolean mPendingInvalidate = false; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); } - /** - * Sets the duration for which all move animations will run. - * - * @param moveDuration The move duration - */ - public void setMoveDuration(long moveDuration) { - mMoveDuration = moveDuration; + public LayoutParams(int width, int height) { + super(width, height); } - /** - * Gets the current duration for which all add animations will run. - * - * @return The current add duration - */ - public long getAddDuration() { - return mAddDuration; + public LayoutParams(MarginLayoutParams source) { + super(source); } - /** - * Sets the duration for which all add animations will run. - * - * @param addDuration The add duration - */ - public void setAddDuration(long addDuration) { - mAddDuration = addDuration; + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); } - /** - * Gets the current duration for which all remove animations will run. - * - * @return The current remove duration - */ - public long getRemoveDuration() { - return mRemoveDuration; + public LayoutParams(LayoutParams source) { + super((ViewGroup.LayoutParams) source); } /** - * Sets the duration for which all remove animations will run. + * Returns true if the view this LayoutParams is attached to needs to have its content + * updated from the corresponding adapter. * - * @param removeDuration The remove duration + * @return true if the view should have its content updated */ - public void setRemoveDuration(long removeDuration) { - mRemoveDuration = removeDuration; + public boolean viewNeedsUpdate() { + return mViewHolder.needsUpdate(); } /** - * Gets the current duration for which all change animations will run. + * Returns true if the view this LayoutParams is attached to is now representing + * potentially invalid data. A LayoutManager should scrap/recycle it. * - * @return The current change duration + * @return true if the view is invalid */ - public long getChangeDuration() { - return mChangeDuration; + public boolean isViewInvalid() { + return mViewHolder.isInvalid(); } /** - * Sets the duration for which all change animations will run. + * Returns true if the adapter data item corresponding to the view this LayoutParams + * is attached to has been removed from the data set. A LayoutManager may choose to + * treat it differently in order to animate its outgoing or disappearing state. * - * @param changeDuration The change duration + * @return true if the item the view corresponds to was removed from the data set */ - public void setChangeDuration(long changeDuration) { - mChangeDuration = changeDuration; + public boolean isItemRemoved() { + return mViewHolder.isRemoved(); } /** - * Internal only: - * Sets the listener that must be called when the animator is finished - * animating the item (or immediately if no animation happens). This is set - * internally and is not intended to be set by external code. + * Returns true if the adapter data item corresponding to the view this LayoutParams + * is attached to has been changed in the data set. A LayoutManager may choose to + * treat it differently in order to animate its changing state. * - * @param listener The listener that must be called. + * @return true if the item the view corresponds to was changed in the data set */ - void setListener(ItemAnimatorListener listener) { - mListener = listener; + public boolean isItemChanged() { + return mViewHolder.isUpdated(); } /** - * Called by the RecyclerView before the layout begins. Item animator should record - * necessary information about the View before it is potentially rebound, moved or removed. - *

    - * The data returned from this method will be passed to the related animate** - * methods. - *

    - * Note that this method may be called after pre-layout phase if LayoutManager adds new - * Views to the layout in pre-layout pass. - *

    - * The default implementation returns an {@link ItemHolderInfo} which holds the bounds of - * the View and the adapter change flags. - * - * @param state The current State of RecyclerView which includes some useful data - * about the layout that will be calculated. - * @param viewHolder The ViewHolder whose information should be recorded. - * @param changeFlags Additional information about what changes happened in the Adapter - * about the Item represented by this ViewHolder. For instance, if - * item is deleted from the adapter, {@link #FLAG_REMOVED} will be set. - * @param payloads The payload list that was previously passed to - * {@link Adapter#notifyItemChanged(int, Object)} or - * {@link Adapter#notifyItemRangeChanged(int, int, Object)}. - * @return An ItemHolderInfo instance that preserves necessary information about the - * ViewHolder. This object will be passed back to related animate** methods - * after layout is complete. - * @see #recordPostLayoutInformation(State, ViewHolder) - * @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) - * @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @deprecated use {@link #getViewLayoutPosition()} or {@link #getViewAdapterPosition()} */ - public @NonNull - ItemHolderInfo recordPreLayoutInformation(@NonNull State state, - @NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags, - @NonNull List payloads) { - return obtainHolderInfo().setFrom(viewHolder); + @Deprecated + public int getViewPosition() { + return mViewHolder.getPosition(); } /** - * Called by the RecyclerView after the layout is complete. Item animator should record - * necessary information about the View's final state. - *

    - * The data returned from this method will be passed to the related animate** - * methods. - *

    - * The default implementation returns an {@link ItemHolderInfo} which holds the bounds of - * the View. + * Returns the adapter position that the view this LayoutParams is attached to corresponds + * to as of latest layout calculation. * - * @param state The current State of RecyclerView which includes some useful data about - * the layout that will be calculated. - * @param viewHolder The ViewHolder whose information should be recorded. - * @return An ItemHolderInfo that preserves necessary information about the ViewHolder. - * This object will be passed back to related animate** methods when - * RecyclerView decides how items should be animated. - * @see #recordPreLayoutInformation(State, ViewHolder, int, List) - * @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) - * @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @return the adapter position this view as of latest layout pass */ - public @NonNull - ItemHolderInfo recordPostLayoutInformation(@NonNull State state, - @NonNull ViewHolder viewHolder) { - return obtainHolderInfo().setFrom(viewHolder); + public int getViewLayoutPosition() { + return mViewHolder.getLayoutPosition(); } /** - * Called by the RecyclerView when a ViewHolder has disappeared from the layout. - *

    - * This means that the View was a child of the LayoutManager when layout started but has - * been removed by the LayoutManager. It might have been removed from the adapter or simply - * become invisible due to other factors. You can distinguish these two cases by checking - * the change flags that were passed to - * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. - *

    - * Note that when a ViewHolder both changes and disappears in the same layout pass, the - * animation callback method which will be called by the RecyclerView depends on the - * ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the - * LayoutManager's decision whether to layout the changed version of a disappearing - * ViewHolder or not. RecyclerView will call - * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateChange} instead of {@code animateDisappearance} if and only if the ItemAnimator - * returns {@code false} from - * {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the - * LayoutManager lays out a new disappearing view that holds the updated information. - * Built-in LayoutManagers try to avoid laying out updated versions of disappearing views. - *

    - * If LayoutManager supports predictive animations, it might provide a target disappear - * location for the View by laying it out in that location. When that happens, - * RecyclerView will call {@link #recordPostLayoutInformation(State, ViewHolder)} and the - * response of that call will be passed to this method as the postLayoutInfo. - *

    - * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation - * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it - * decides not to animate the view). - * - * @param viewHolder The ViewHolder which should be animated - * @param preLayoutInfo The information that was returned from - * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. - * @param postLayoutInfo The information that was returned from - * {@link #recordPostLayoutInformation(State, ViewHolder)}. Might be - * null if the LayoutManager did not layout the item. - * @return true if a later call to {@link #runPendingAnimations()} is requested, - * false otherwise. + * @deprecated This method is confusing when nested adapters are used. + * If you are calling from the context of an {@link Adapter}, + * use {@link #getBindingAdapterPosition()}. If you need the position that + * {@link RecyclerView} sees, use {@link #getAbsoluteAdapterPosition()}. */ - public abstract boolean animateDisappearance(@NonNull ViewHolder viewHolder, - @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo); + @Deprecated + public int getViewAdapterPosition() { + return mViewHolder.getBindingAdapterPosition(); + } /** - * Called by the RecyclerView when a ViewHolder is added to the layout. - *

    - * In detail, this means that the ViewHolder was not a child when the layout started - * but has been added by the LayoutManager. It might be newly added to the adapter or - * simply become visible due to other factors. - *

    - * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation - * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it - * decides not to animate the view). + * Returns the up-to-date adapter position that the view this LayoutParams is attached to + * corresponds to in the {@link RecyclerView}. If the {@link RecyclerView} has an + * {@link Adapter} that merges other adapters, this position will be with respect to the + * adapter that is assigned to the {@link RecyclerView}. * - * @param viewHolder The ViewHolder which should be animated - * @param preLayoutInfo The information that was returned from - * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. - * Might be null if Item was just added to the adapter or - * LayoutManager does not support predictive animations or it could - * not predict that this ViewHolder will become visible. - * @param postLayoutInfo The information that was returned from {@link - * #recordPreLayoutInformation(State, ViewHolder, int, List)}. - * @return true if a later call to {@link #runPendingAnimations()} is requested, - * false otherwise. + * @return the up-to-date adapter position this view with respect to the RecyclerView. It + * may return {@link RecyclerView#NO_POSITION} if item represented by this View has been + * removed or + * its up-to-date position cannot be calculated. */ - public abstract boolean animateAppearance(@NonNull ViewHolder viewHolder, - @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo); + public int getAbsoluteAdapterPosition() { + return mViewHolder.getAbsoluteAdapterPosition(); + } /** - * Called by the RecyclerView when a ViewHolder is present in both before and after the - * layout and RecyclerView has not received a {@link Adapter#notifyItemChanged(int)} call - * for it or a {@link Adapter#notifyDataSetChanged()} call. - *

    - * This ViewHolder still represents the same data that it was representing when the layout - * started but its position / size may be changed by the LayoutManager. - *

    - * If the Item's layout position didn't change, RecyclerView still calls this method because - * it does not track this information (or does not necessarily know that an animation is - * not required). Your ItemAnimator should handle this case and if there is nothing to - * animate, it should call {@link #dispatchAnimationFinished(ViewHolder)} and return - * false. - *

    - * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation - * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it - * decides not to animate the view). + * Returns the up-to-date adapter position that the view this LayoutParams is attached to + * corresponds to with respect to the {@link Adapter} that bound this View. * - * @param viewHolder The ViewHolder which should be animated - * @param preLayoutInfo The information that was returned from - * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. - * @param postLayoutInfo The information that was returned from {@link - * #recordPreLayoutInformation(State, ViewHolder, int, List)}. - * @return true if a later call to {@link #runPendingAnimations()} is requested, - * false otherwise. + * @return the up-to-date adapter position this view relative to the {@link Adapter} that + * bound this View. It may return {@link RecyclerView#NO_POSITION} if item represented by + * this View has been removed or its up-to-date position cannot be calculated. */ - public abstract boolean animatePersistence(@NonNull ViewHolder viewHolder, - @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo); + public int getBindingAdapterPosition() { + return mViewHolder.getBindingAdapterPosition(); + } + } + + /** + * Observer base class for watching changes to an {@link Adapter}. + * See {@link Adapter#registerAdapterDataObserver(AdapterDataObserver)}. + */ + public abstract static class AdapterDataObserver { + public void onChanged() { + // Do nothing + } + + public void onItemRangeChanged(int positionStart, int itemCount) { + // do nothing + } + + public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { + // fallback to onItemRangeChanged(positionStart, itemCount) if app + // does not override this method. + onItemRangeChanged(positionStart, itemCount); + } + + public void onItemRangeInserted(int positionStart, int itemCount) { + // do nothing + } + + public void onItemRangeRemoved(int positionStart, int itemCount) { + // do nothing + } + + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + // do nothing + } /** - * Called by the RecyclerView when an adapter item is present both before and after the - * layout and RecyclerView has received a {@link Adapter#notifyItemChanged(int)} call - * for it. This method may also be called when - * {@link Adapter#notifyDataSetChanged()} is called and adapter has stable ids so that - * RecyclerView could still rebind views to the same ViewHolders. If viewType changes when - * {@link Adapter#notifyDataSetChanged()} is called, this method will not be called, - * instead, {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)} will be - * called for the new ViewHolder and the old one will be recycled. - *

    - * If this method is called due to a {@link Adapter#notifyDataSetChanged()} call, there is - * a good possibility that item contents didn't really change but it is rebound from the - * adapter. {@link DefaultItemAnimator} will skip animating the View if its location on the - * screen didn't change and your animator should handle this case as well and avoid creating - * unnecessary animations. - *

    - * When an item is updated, ItemAnimator has a chance to ask RecyclerView to keep the - * previous presentation of the item as-is and supply a new ViewHolder for the updated - * presentation (see: {@link #canReuseUpdatedViewHolder(ViewHolder, List)}. - * This is useful if you don't know the contents of the Item and would like - * to cross-fade the old and the new one ({@link DefaultItemAnimator} uses this technique). - *

    - * When you are writing a custom item animator for your layout, it might be more performant - * and elegant to re-use the same ViewHolder and animate the content changes manually. - *

    - * When {@link Adapter#notifyItemChanged(int)} is called, the Item's view type may change. - * If the Item's view type has changed or ItemAnimator returned false for - * this ViewHolder when {@link #canReuseUpdatedViewHolder(ViewHolder, List)} was called, the - * oldHolder and newHolder will be different ViewHolder instances - * which represent the same Item. In that case, only the new ViewHolder is visible - * to the LayoutManager but RecyclerView keeps old ViewHolder attached for animations. - *

    - * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} for each distinct - * ViewHolder when their animation is complete - * (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it decides not to - * animate the view). - *

    - * If oldHolder and newHolder are the same instance, you should call - * {@link #dispatchAnimationFinished(ViewHolder)} only once. - *

    - * Note that when a ViewHolder both changes and disappears in the same layout pass, the - * animation callback method which will be called by the RecyclerView depends on the - * ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the - * LayoutManager's decision whether to layout the changed version of a disappearing - * ViewHolder or not. RecyclerView will call - * {@code animateChange} instead of - * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateDisappearance} if and only if the ItemAnimator returns {@code false} from - * {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the - * LayoutManager lays out a new disappearing view that holds the updated information. - * Built-in LayoutManagers try to avoid laying out updated versions of disappearing views. + * Called when the {@link Adapter.StateRestorationPolicy} of the {@link Adapter} changed. + * When this method is called, the Adapter might be ready to restore its state if it has + * not already been restored. * - * @param oldHolder The ViewHolder before the layout is started, might be the same - * instance with newHolder. - * @param newHolder The ViewHolder after the layout is finished, might be the same - * instance with oldHolder. - * @param preLayoutInfo The information that was returned from - * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. - * @param postLayoutInfo The information that was returned from {@link - * #recordPreLayoutInformation(State, ViewHolder, int, List)}. - * @return true if a later call to {@link #runPendingAnimations()} is requested, - * false otherwise. + * @see Adapter#getStateRestorationPolicy() + * @see Adapter#setStateRestorationPolicy(Adapter.StateRestorationPolicy) */ - public abstract boolean animateChange(@NonNull ViewHolder oldHolder, - @NonNull ViewHolder newHolder, - @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo); + public void onStateRestorationPolicyChanged() { + // do nothing + } + } - /** - * Called when there are pending animations waiting to be started. This state - * is governed by the return values from - * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateAppearance()}, - * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateChange()} - * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animatePersistence()}, and - * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateDisappearance()}, which inform the RecyclerView that the ItemAnimator wants to be - * called later to start the associated animations. runPendingAnimations() will be scheduled - * to be run on the next frame. - */ - public abstract void runPendingAnimations(); + /** + * Base class for smooth scrolling. Handles basic tracking of the target view position and + * provides methods to trigger a programmatic scroll. + * + *

    An instance of SmoothScroller is only intended to be used once. You should create a new + * instance for each call to {@link LayoutManager#startSmoothScroll(SmoothScroller)}. + * + * @see LinearSmoothScroller + */ + public abstract static class SmoothScroller { + + private int mTargetPosition = RecyclerView.NO_POSITION; + + private RecyclerView mRecyclerView; + + private LayoutManager mLayoutManager; + + private boolean mPendingInitialRun; + + private boolean mRunning; + + private View mTargetView; + + private final Action mRecyclingAction; + + private boolean mStarted; + + public SmoothScroller() { + mRecyclingAction = new Action(0, 0); + } /** - * Method called when an animation on a view should be ended immediately. - * This could happen when other events, like scrolling, occur, so that - * animating views can be quickly put into their proper end locations. - * Implementations should ensure that any animations running on the item - * are canceled and affected properties are set to their end values. - * Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished - * animation since the animations are effectively done when this method is called. + * Starts a smooth scroll for the given target position. + *

    In each animation step, {@link RecyclerView} will check + * for the target view and call either + * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or + * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)} until + * SmoothScroller is stopped.

    * - * @param item The item for which an animation should be stopped. + *

    Note that if RecyclerView finds the target view, it will automatically stop the + * SmoothScroller. This does not mean that scroll will stop, it only means it will + * stop calling SmoothScroller in each animation step.

    */ - public abstract void endAnimation(@NonNull ViewHolder item); + void start(RecyclerView recyclerView, LayoutManager layoutManager) { - /** - * Method called when all item animations should be ended immediately. - * This could happen when other events, like scrolling, occur, so that - * animating views can be quickly put into their proper end locations. - * Implementations should ensure that any animations running on any items - * are canceled and affected properties are set to their end values. - * Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished - * animation since the animations are effectively done when this method is called. - */ - public abstract void endAnimations(); + // Stop any previous ViewFlinger animations now because we are about to start a new one. + recyclerView.mViewFlinger.stop(); - /** - * Method which returns whether there are any item animations currently running. - * This method can be used to determine whether to delay other actions until - * animations end. - * - * @return true if there are any item animations currently running, false otherwise. - */ - public abstract boolean isRunning(); + if (mStarted) { + Log.w(TAG, "An instance of " + this.getClass().getSimpleName() + " was started " + + "more than once. Each instance of" + this.getClass().getSimpleName() + " " + + "is intended to only be used once. You should create a new instance for " + + "each use."); + } + + mRecyclerView = recyclerView; + mLayoutManager = layoutManager; + if (mTargetPosition == RecyclerView.NO_POSITION) { + throw new IllegalArgumentException("Invalid target position"); + } + mRecyclerView.mState.mTargetPosition = mTargetPosition; + mRunning = true; + mPendingInitialRun = true; + mTargetView = findViewByPosition(getTargetPosition()); + onStart(); + mRecyclerView.mViewFlinger.postOnAnimation(); + + mStarted = true; + } + + public void setTargetPosition(int targetPosition) { + mTargetPosition = targetPosition; + } /** - * Method to be called by subclasses when an animation is finished. - *

    - * For each call RecyclerView makes to - * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateAppearance()}, - * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animatePersistence()}, or - * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateDisappearance()}, there - * should - * be a matching {@link #dispatchAnimationFinished(ViewHolder)} call by the subclass. + * Compute the scroll vector for a given target position. *

    - * For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateChange()}, subclass should call this method for both the oldHolder - * and newHolder (if they are not the same instance). + * This method can return null if the layout manager cannot calculate a scroll vector + * for the given position (e.g. it has no current scroll position). * - * @param viewHolder The ViewHolder whose animation is finished. - * @see #onAnimationFinished(ViewHolder) + * @param targetPosition the position to which the scroller is scrolling + * @return the scroll vector for a given target position */ - public final void dispatchAnimationFinished(@NonNull ViewHolder viewHolder) { - onAnimationFinished(viewHolder); - if (mListener != null) { - mListener.onAnimationFinished(viewHolder); + @Nullable + public PointF computeScrollVectorForPosition(int targetPosition) { + LayoutManager layoutManager = getLayoutManager(); + if (layoutManager instanceof ScrollVectorProvider) { + return ((ScrollVectorProvider) layoutManager) + .computeScrollVectorForPosition(targetPosition); } + Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager" + + " does not implement " + ScrollVectorProvider.class.getCanonicalName()); + return null; } /** - * Called after {@link #dispatchAnimationFinished(ViewHolder)} is called by the - * ItemAnimator. - * - * @param viewHolder The ViewHolder whose animation is finished. There might still be other - * animations running on this ViewHolder. - * @see #dispatchAnimationFinished(ViewHolder) + * @return The LayoutManager to which this SmoothScroller is attached. Will return + * null after the SmoothScroller is stopped. */ - public void onAnimationFinished(@NonNull ViewHolder viewHolder) { + @Nullable + public LayoutManager getLayoutManager() { + return mLayoutManager; } /** - * Method to be called by subclasses when an animation is started. - *

    - * For each call RecyclerView makes to - * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateAppearance()}, - * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animatePersistence()}, or - * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateDisappearance()}, there should be a matching - * {@link #dispatchAnimationStarted(ViewHolder)} call by the subclass. - *

    - * For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) - * animateChange()}, subclass should call this method for both the oldHolder - * and newHolder (if they are not the same instance). - *

    - * If your ItemAnimator decides not to animate a ViewHolder, it should call - * {@link #dispatchAnimationFinished(ViewHolder)} without calling - * {@link #dispatchAnimationStarted(ViewHolder)}. - * - * @param viewHolder The ViewHolder whose animation is starting. - * @see #onAnimationStarted(ViewHolder) + * Stops running the SmoothScroller in each animation callback. Note that this does not + * cancel any existing {@link Action} updated by + * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or + * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)}. */ - public final void dispatchAnimationStarted(@NonNull ViewHolder viewHolder) { - onAnimationStarted(viewHolder); + protected final void stop() { + if (!mRunning) { + return; + } + mRunning = false; + onStop(); + mRecyclerView.mState.mTargetPosition = RecyclerView.NO_POSITION; + mTargetView = null; + mTargetPosition = RecyclerView.NO_POSITION; + mPendingInitialRun = false; + // trigger a cleanup + mLayoutManager.onSmoothScrollerStopped(this); + // clear references to avoid any potential leak by a custom smooth scroller + mLayoutManager = null; + mRecyclerView = null; } /** - * Called when a new animation is started on the given ViewHolder. + * Returns true if SmoothScroller has been started but has not received the first + * animation + * callback yet. * - * @param viewHolder The ViewHolder which started animating. Note that the ViewHolder - * might already be animating and this might be another animation. - * @see #dispatchAnimationStarted(ViewHolder) + * @return True if this SmoothScroller is waiting to start */ - public void onAnimationStarted(@NonNull ViewHolder viewHolder) { + public boolean isPendingInitialRun() { + return mPendingInitialRun; + } + + /** + * @return True if SmoothScroller is currently active + */ + public boolean isRunning() { + return mRunning; } /** - * Like {@link #isRunning()}, this method returns whether there are any item - * animations currently running. Additionally, the listener passed in will be called - * when there are no item animations running, either immediately (before the method - * returns) if no animations are currently running, or when the currently running - * animations are {@link #dispatchAnimationsFinished() finished}. - * - *

    Note that the listener is transient - it is either called immediately and not - * stored at all, or stored only until it is called when running animations - * are finished sometime later.

    + * Returns the adapter position of the target item * - * @param listener A listener to be called immediately if no animations are running - * or later when currently-running animations have finished. A null - * listener is - * equivalent to calling {@link #isRunning()}. - * @return true if there are any item animations currently running, false otherwise. + * @return Adapter position of the target item or + * {@link RecyclerView#NO_POSITION} if no target view is set. */ - public final boolean isRunning(@Nullable ItemAnimatorFinishedListener listener) { - boolean running = isRunning(); - if (listener != null) { - if (!running) { - listener.onAnimationsFinished(); + public int getTargetPosition() { + return mTargetPosition; + } + + void onAnimation(int dx, int dy) { + final RecyclerView recyclerView = mRecyclerView; + if (mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) { + stop(); + } + + // The following if block exists to have the LayoutManager scroll 1 pixel in the correct + // direction in order to cause the LayoutManager to draw two pages worth of views so + // that the target view may be found before scrolling any further. This is done to + // prevent an initial scroll distance from scrolling past the view, which causes a + // jittery looking animation. + if (mPendingInitialRun && mTargetView == null && mLayoutManager != null) { + PointF pointF = computeScrollVectorForPosition(mTargetPosition); + if (pointF != null && (pointF.x != 0 || pointF.y != 0)) { + recyclerView.scrollStep( + (int) Math.signum(pointF.x), + (int) Math.signum(pointF.y), + null); + } + } + + mPendingInitialRun = false; + + if (mTargetView != null) { + // verify target position + if (getChildPosition(mTargetView) == mTargetPosition) { + onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction); + mRecyclingAction.runIfNecessary(recyclerView); + stop(); } else { - mFinishedListeners.add(listener); + Log.e(TAG, "Passed over target position while smooth scrolling."); + mTargetView = null; + } + } + if (mRunning) { + onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction); + boolean hadJumpTarget = mRecyclingAction.hasJumpTarget(); + mRecyclingAction.runIfNecessary(recyclerView); + if (hadJumpTarget) { + // It is not stopped so needs to be restarted + if (mRunning) { + mPendingInitialRun = true; + recyclerView.mViewFlinger.postOnAnimation(); + } } } - return running; } - - /** - * When an item is changed, ItemAnimator can decide whether it wants to re-use - * the same ViewHolder for animations or RecyclerView should create a copy of the - * item and ItemAnimator will use both to run the animation (e.g. cross-fade). - *

    - * Note that this method will only be called if the {@link ViewHolder} still has the same - * type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive - * both {@link ViewHolder}s in the - * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method. - *

    - * If your application is using change payloads, you can override - * {@link #canReuseUpdatedViewHolder(ViewHolder, List)} to decide based on payloads. - * - * @param viewHolder The ViewHolder which represents the changed item's old content. - * @return True if RecyclerView should just rebind to the same ViewHolder or false if - * RecyclerView should create a new ViewHolder and pass this ViewHolder to the - * ItemAnimator to animate. Default implementation returns true. - * @see #canReuseUpdatedViewHolder(ViewHolder, List) + + /** + * @see RecyclerView#getChildLayoutPosition(android.view.View) */ - public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder) { - return true; + public int getChildPosition(View view) { + return mRecyclerView.getChildLayoutPosition(view); } /** - * When an item is changed, ItemAnimator can decide whether it wants to re-use - * the same ViewHolder for animations or RecyclerView should create a copy of the - * item and ItemAnimator will use both to run the animation (e.g. cross-fade). - *

    - * Note that this method will only be called if the {@link ViewHolder} still has the same - * type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive - * both {@link ViewHolder}s in the - * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method. - * - * @param viewHolder The ViewHolder which represents the changed item's old content. - * @param payloads A non-null list of merged payloads that were sent with change - * notifications. Can be empty if the adapter is invalidated via - * {@link RecyclerView.Adapter#notifyDataSetChanged()}. The same list of - * payloads will be passed into - * {@link RecyclerView.Adapter#onBindViewHolder(ViewHolder, int, List)} - * method if this method returns true. - * @return True if RecyclerView should just rebind to the same ViewHolder or false if - * RecyclerView should create a new ViewHolder and pass this ViewHolder to the - * ItemAnimator to animate. Default implementation calls - * {@link #canReuseUpdatedViewHolder(ViewHolder)}. - * @see #canReuseUpdatedViewHolder(ViewHolder) + * @see RecyclerView.LayoutManager#getChildCount() */ - public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder, - @NonNull List payloads) { - return canReuseUpdatedViewHolder(viewHolder); + public int getChildCount() { + return mRecyclerView.mLayout.getChildCount(); } /** - * This method should be called by ItemAnimator implementations to notify - * any listeners that all pending and active item animations are finished. + * @see RecyclerView.LayoutManager#findViewByPosition(int) */ - public final void dispatchAnimationsFinished() { - int count = mFinishedListeners.size(); - for (int i = 0; i < count; ++i) { - mFinishedListeners.get(i).onAnimationsFinished(); + public View findViewByPosition(int position) { + return mRecyclerView.mLayout.findViewByPosition(position); + } + + /** + * @see RecyclerView#scrollToPosition(int) + * @deprecated Use {@link Action#jumpTo(int)}. + */ + @Deprecated + public void instantScrollToPosition(int position) { + mRecyclerView.scrollToPosition(position); + } + + protected void onChildAttachedToWindow(View child) { + if (getChildPosition(child) == getTargetPosition()) { + mTargetView = child; + if (DEBUG) { + Log.d(TAG, "smooth scroll target view has been attached"); + } } - mFinishedListeners.clear(); } /** - * Returns a new {@link ItemHolderInfo} which will be used to store information about the - * ViewHolder. This information will later be passed into animate** methods. - *

    - * You can override this method if you want to extend {@link ItemHolderInfo} and provide - * your own instances. + * Normalizes the vector. * - * @return A new {@link ItemHolderInfo}. + * @param scrollVector The vector that points to the target scroll position */ - @NonNull - public ItemHolderInfo obtainHolderInfo() { - return new ItemHolderInfo(); + protected void normalize(@NonNull PointF scrollVector) { + final float magnitude = (float) Math.sqrt(scrollVector.x * scrollVector.x + + scrollVector.y * scrollVector.y); + scrollVector.x /= magnitude; + scrollVector.y /= magnitude; } /** - * The set of flags that might be passed to - * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * Called when smooth scroll is started. This might be a good time to do setup. */ - @IntDef(flag = true, value = { - FLAG_CHANGED, FLAG_REMOVED, FLAG_MOVED, FLAG_INVALIDATED, - FLAG_APPEARED_IN_PRE_LAYOUT - }) - @Retention(RetentionPolicy.SOURCE) - public @interface AdapterChanges { - } + protected abstract void onStart(); /** - * The interface to be implemented by listeners to animation events from this - * ItemAnimator. This is used internally and is not intended for developers to - * create directly. + * Called when smooth scroller is stopped. This is a good place to cleanup your state etc. + * + * @see #stop() */ - interface ItemAnimatorListener { - void onAnimationFinished(@NonNull ViewHolder item); - } + protected abstract void onStop(); /** - * This interface is used to inform listeners when all pending or running animations - * in an ItemAnimator are finished. This can be used, for example, to delay an action - * in a data set until currently-running animations are complete. + *

    RecyclerView will call this method each time it scrolls until it can find the target + * position in the layout.

    + *

    SmoothScroller should check dx, dy and if scroll should be changed, update the + * provided {@link Action} to define the next scroll.

    * - * @see #isRunning(ItemAnimatorFinishedListener) + * @param dx Last scroll amount horizontally + * @param dy Last scroll amount vertically + * @param state Transient state of RecyclerView + * @param action If you want to trigger a new smooth scroll and cancel the previous one, + * update this object. */ - public interface ItemAnimatorFinishedListener { - /** - * Notifies when all pending or running animations in an ItemAnimator are finished. - */ - void onAnimationsFinished(); - } + protected abstract void onSeekTargetStep(@Px int dx, @Px int dy, @NonNull State state, + @NonNull Action action); /** - * A simple data structure that holds information about an item's bounds. - * This information is used in calculating item animations. Default implementation of - * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)} and - * {@link #recordPostLayoutInformation(RecyclerView.State, ViewHolder)} returns this data - * structure. You can extend this class if you would like to keep more information about - * the Views. - *

    - * If you want to provide your own implementation but still use `super` methods to record - * basic information, you can override {@link #obtainHolderInfo()} to provide your own - * instances. + * Called when the target position is laid out. This is the last callback SmoothScroller + * will receive and it should update the provided {@link Action} to define the scroll + * details towards the target view. + * + * @param targetView The view element which render the target position. + * @param state Transient state of RecyclerView + * @param action Action instance that you should update to define final scroll action + * towards the targetView */ - public static class ItemHolderInfo { + protected abstract void onTargetFound(@NonNull View targetView, @NonNull State state, + @NonNull Action action); + + /** + * Holds information about a smooth scroll request by a {@link SmoothScroller}. + */ + public static class Action { + + public static final int UNDEFINED_DURATION = RecyclerView.UNDEFINED_DURATION; + + private int mDx; + + private int mDy; + + private int mDuration; + + private int mJumpToPosition = NO_POSITION; + + private Interpolator mInterpolator; + + private boolean mChanged = false; + + // we track this variable to inform custom implementer if they are updating the action + // in every animation callback + private int mConsecutiveUpdates = 0; + + /** + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + */ + public Action(@Px int dx, @Px int dy) { + this(dx, dy, UNDEFINED_DURATION, null); + } + + /** + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + * @param duration Duration of the animation in milliseconds + */ + public Action(@Px int dx, @Px int dy, int duration) { + this(dx, dy, duration, null); + } + + /** + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + * @param duration Duration of the animation in milliseconds + * @param interpolator Interpolator to be used when calculating scroll position in each + * animation step + */ + public Action(@Px int dx, @Px int dy, int duration, + @Nullable Interpolator interpolator) { + mDx = dx; + mDy = dy; + mDuration = duration; + mInterpolator = interpolator; + } + + /** + * Instead of specifying pixels to scroll, use the target position to jump using + * {@link RecyclerView#scrollToPosition(int)}. + *

    + * You may prefer using this method if scroll target is really far away and you prefer + * to jump to a location and smooth scroll afterwards. + *

    + * Note that calling this method takes priority over other update methods such as + * {@link #update(int, int, int, Interpolator)}, {@link #setX(float)}, + * {@link #setY(float)} and #{@link #setInterpolator(Interpolator)}. If you call + * {@link #jumpTo(int)}, the other changes will not be considered for this animation + * frame. + * + * @param targetPosition The target item position to scroll to using instant scrolling. + */ + public void jumpTo(int targetPosition) { + mJumpToPosition = targetPosition; + } + + boolean hasJumpTarget() { + return mJumpToPosition >= 0; + } + + void runIfNecessary(RecyclerView recyclerView) { + if (mJumpToPosition >= 0) { + final int position = mJumpToPosition; + mJumpToPosition = NO_POSITION; + recyclerView.jumpToPositionForSmoothScroller(position); + mChanged = false; + return; + } + if (mChanged) { + validate(); + recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration, mInterpolator); + mConsecutiveUpdates++; + if (mConsecutiveUpdates > 10) { + // A new action is being set in every animation step. This looks like a bad + // implementation. Inform developer. + Log.e(TAG, "Smooth Scroll action is being updated too frequently. Make sure" + + " you are not changing it unless necessary"); + } + mChanged = false; + } else { + mConsecutiveUpdates = 0; + } + } + + private void validate() { + if (mInterpolator != null && mDuration < 1) { + throw new IllegalStateException("If you provide an interpolator, you must" + + " set a positive duration"); + } else if (mDuration < 1) { + throw new IllegalStateException("Scroll duration must be a positive number"); + } + } + + @Px + public int getDx() { + return mDx; + } + + public void setDx(@Px int dx) { + mChanged = true; + mDx = dx; + } + + @Px + public int getDy() { + return mDy; + } - /** - * The left edge of the View (excluding decorations) - */ - public int left; + public void setDy(@Px int dy) { + mChanged = true; + mDy = dy; + } - /** - * The top edge of the View (excluding decorations) - */ - public int top; + public int getDuration() { + return mDuration; + } - /** - * The right edge of the View (excluding decorations) - */ - public int right; + public void setDuration(int duration) { + mChanged = true; + mDuration = duration; + } - /** - * The bottom edge of the View (excluding decorations) - */ - public int bottom; + @Nullable + public Interpolator getInterpolator() { + return mInterpolator; + } /** - * The change flags that were passed to - * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)}. + * Sets the interpolator to calculate scroll steps + * + * @param interpolator The interpolator to use. If you specify an interpolator, you must + * also set the duration. + * @see #setDuration(int) */ - @AdapterChanges - public int changeFlags; - - public ItemHolderInfo() { + public void setInterpolator(@Nullable Interpolator interpolator) { + mChanged = true; + mInterpolator = interpolator; } /** - * Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from - * the given ViewHolder. Clears all {@link #changeFlags}. + * Updates the action with given parameters. * - * @param holder The ViewHolder whose bounds should be copied. - * @return This {@link ItemHolderInfo} + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + * @param duration Duration of the animation in milliseconds + * @param interpolator Interpolator to be used when calculating scroll position in each + * animation step */ - @NonNull - public ItemHolderInfo setFrom(@NonNull RecyclerView.ViewHolder holder) { - return setFrom(holder, 0); + public void update(@Px int dx, @Px int dy, int duration, + @Nullable Interpolator interpolator) { + mDx = dx; + mDy = dy; + mDuration = duration; + mInterpolator = interpolator; + mChanged = true; } + } + /** + * An interface which is optionally implemented by custom {@link RecyclerView.LayoutManager} + * to provide a hint to a {@link SmoothScroller} about the location of the target position. + */ + public interface ScrollVectorProvider { /** - * Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from - * the given ViewHolder and sets the {@link #changeFlags} to the given flags parameter. + * Should calculate the vector that points to the direction where the target position + * can be found. + *

    + * This method is used by the {@link LinearSmoothScroller} to initiate a scroll towards + * the target position. + *

    + * The magnitude of the vector is not important. It is always normalized before being + * used by the {@link LinearSmoothScroller}. + *

    + * LayoutManager should not check whether the position exists in the adapter or not. * - * @param holder The ViewHolder whose bounds should be copied. - * @param flags The adapter change flags that were passed into - * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, - * List)}. - * @return This {@link ItemHolderInfo} + * @param targetPosition the target position to which the returned vector should point + * @return the scroll vector for a given position. */ - @NonNull - public ItemHolderInfo setFrom(@NonNull RecyclerView.ViewHolder holder, - @AdapterChanges int flags) { - View view = holder.itemView; - left = view.getLeft(); - top = view.getTop(); - right = view.getRight(); - bottom = view.getBottom(); - return this; - } + @Nullable + PointF computeScrollVectorForPosition(int targetPosition); } } - // Effectively private. Set to default to avoid synthetic accessor. - class ViewFlinger implements Runnable { - OverScroller mOverScroller; - Interpolator mInterpolator = sQuinticInterpolator; - private int mLastFlingX; - private int mLastFlingY; - // When set to true, postOnAnimation callbacks are delayed until the run method completes - private boolean mEatRunOnAnimationRequest; - - // Tracks if postAnimationCallback should be re-attached when it is done - private boolean mReSchedulePostAnimationCallback; - - ViewFlinger() { - mOverScroller = new OverScroller(getContext(), sQuinticInterpolator); + static class AdapterDataObservable extends Observable { + public boolean hasObservers() { + return !mObservers.isEmpty(); } - @Override - public void run() { - if (mLayout == null) { - stop(); - return; // no layout, cannot scroll. - } - - mReSchedulePostAnimationCallback = false; - mEatRunOnAnimationRequest = true; - - consumePendingUpdateOperations(); - - // TODO(72745539): After reviewing the code, it seems to me we may actually want to - // update the reference to the OverScroller after onAnimation. It looks to me like - // it is possible that a new OverScroller could be created (due to a new Interpolator - // being used), when the current OverScroller knows it's done after - // scroller.computeScrollOffset() is called. If that happens, and we don't update the - // reference, it seems to me that we could prematurely stop the newly created scroller - // due to setScrollState(SCROLL_STATE_IDLE) being called below. - - // Keep a local reference so that if it is changed during onAnimation method, it won't - // cause unexpected behaviors - OverScroller scroller = mOverScroller; - if (scroller.computeScrollOffset()) { - int x = scroller.getCurrX(); - int y = scroller.getCurrY(); - int unconsumedX = x - mLastFlingX; - int unconsumedY = y - mLastFlingY; - mLastFlingX = x; - mLastFlingY = y; - - unconsumedX = consumeFlingInHorizontalStretch(unconsumedX); - unconsumedY = consumeFlingInVerticalStretch(unconsumedY); - - int consumedX = 0; - int consumedY = 0; - - // Nested Pre Scroll - mReusableIntPair[0] = 0; - mReusableIntPair[1] = 0; - if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null, - TYPE_NON_TOUCH)) { - unconsumedX -= mReusableIntPair[0]; - unconsumedY -= mReusableIntPair[1]; - } - - // Based on movement, we may want to trigger the hiding of existing over scroll - // glows. - if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { - considerReleasingGlowsOnScroll(unconsumedX, unconsumedY); - } - - // Local Scroll - if (mAdapter != null) { - mReusableIntPair[0] = 0; - mReusableIntPair[1] = 0; - scrollStep(unconsumedX, unconsumedY, mReusableIntPair); - consumedX = mReusableIntPair[0]; - consumedY = mReusableIntPair[1]; - unconsumedX -= consumedX; - unconsumedY -= consumedY; - - // If SmoothScroller exists, this ViewFlinger was started by it, so we must - // report back to SmoothScroller. - SmoothScroller smoothScroller = mLayout.mSmoothScroller; - if (smoothScroller != null && !smoothScroller.isPendingInitialRun() - && smoothScroller.isRunning()) { - int adapterSize = mState.getItemCount(); - if (adapterSize == 0) { - smoothScroller.stop(); - } else if (smoothScroller.getTargetPosition() >= adapterSize) { - smoothScroller.setTargetPosition(adapterSize - 1); - smoothScroller.onAnimation(consumedX, consumedY); - } else { - smoothScroller.onAnimation(consumedX, consumedY); - } - } - } - - if (!mItemDecorations.isEmpty()) { - invalidate(); - } - - // Nested Post Scroll - mReusableIntPair[0] = 0; - mReusableIntPair[1] = 0; - dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null, - TYPE_NON_TOUCH, mReusableIntPair); - unconsumedX -= mReusableIntPair[0]; - unconsumedY -= mReusableIntPair[1]; - - if (consumedX != 0 || consumedY != 0) { - dispatchOnScrolled(consumedX, consumedY); - } - - if (!awakenScrollBars()) { - invalidate(); - } - - // We are done scrolling if scroller is finished, or for both the x and y dimension, - // we are done scrolling or we can't scroll further (we know we can't scroll further - // when we have unconsumed scroll distance). It's possible that we don't need - // to also check for scroller.isFinished() at all, but no harm in doing so in case - // of old bugs in Overscroller. - boolean scrollerFinishedX = scroller.getCurrX() == scroller.getFinalX(); - boolean scrollerFinishedY = scroller.getCurrY() == scroller.getFinalY(); - boolean doneScrolling = scroller.isFinished() - || ((scrollerFinishedX || unconsumedX != 0) - && (scrollerFinishedY || unconsumedY != 0)); - - // Get the current smoothScroller. It may have changed by this point and we need to - // make sure we don't stop scrolling if it has changed and it's pending an initial - // run. - SmoothScroller smoothScroller = mLayout.mSmoothScroller; - boolean smoothScrollerPending = - smoothScroller != null && smoothScroller.isPendingInitialRun(); - - if (!smoothScrollerPending && doneScrolling) { - // If we are done scrolling and the layout's SmoothScroller is not pending, - // do the things we do at the end of a scroll and don't postOnAnimation. - - if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { - int vel = (int) scroller.getCurrVelocity(); - int velX = unconsumedX < 0 ? -vel : unconsumedX > 0 ? vel : 0; - int velY = unconsumedY < 0 ? -vel : unconsumedY > 0 ? vel : 0; - absorbGlows(velX, velY); - } - - if (ALLOW_THREAD_GAP_WORK) { - mPrefetchRegistry.clearPrefetchPositions(); - } - } else { - // Otherwise continue the scroll. - - postOnAnimation(); - if (mGapWorker != null) { - mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY); - } - } + public void notifyChanged() { + // since onChanged() is implemented by the app, it could do anything, including + // removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onChanged(); } + } - SmoothScroller smoothScroller = mLayout.mSmoothScroller; - // call this after the onAnimation is complete not to have inconsistent callbacks etc. - if (smoothScroller != null && smoothScroller.isPendingInitialRun()) { - smoothScroller.onAnimation(0, 0); + public void notifyStateRestorationPolicyChanged() { + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onStateRestorationPolicyChanged(); } + } - mEatRunOnAnimationRequest = false; - if (mReSchedulePostAnimationCallback) { - internalPostOnAnimation(); - } else { - setScrollState(SCROLL_STATE_IDLE); - stopNestedScroll(TYPE_NON_TOUCH); + public void notifyItemRangeChanged(int positionStart, int itemCount) { + notifyItemRangeChanged(positionStart, itemCount, null); + } + + public void notifyItemRangeChanged(int positionStart, int itemCount, + @Nullable Object payload) { + // since onItemRangeChanged() is implemented by the app, it could do anything, including + // removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload); } } - void postOnAnimation() { - if (mEatRunOnAnimationRequest) { - mReSchedulePostAnimationCallback = true; - } else { - internalPostOnAnimation(); + public void notifyItemRangeInserted(int positionStart, int itemCount) { + // since onItemRangeInserted() is implemented by the app, it could do anything, + // including removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onItemRangeInserted(positionStart, itemCount); } } - private void internalPostOnAnimation() { - removeCallbacks(this); - ViewCompat.postOnAnimation(RecyclerView.this, this); + public void notifyItemRangeRemoved(int positionStart, int itemCount) { + // since onItemRangeRemoved() is implemented by the app, it could do anything, including + // removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onItemRangeRemoved(positionStart, itemCount); + } } - public void fling(int velocityX, int velocityY) { - setScrollState(SCROLL_STATE_SETTLING); - mLastFlingX = mLastFlingY = 0; - // Because you can't define a custom interpolator for flinging, we should make sure we - // reset ourselves back to the teh default interpolator in case a different call - // changed our interpolator. - if (mInterpolator != sQuinticInterpolator) { - mInterpolator = sQuinticInterpolator; - mOverScroller = new OverScroller(getContext(), sQuinticInterpolator); + public void notifyItemMoved(int fromPosition, int toPosition) { + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onItemRangeMoved(fromPosition, toPosition, 1); } - mOverScroller.fling(0, 0, velocityX, velocityY, - Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); - postOnAnimation(); + } + } + + /** + * This is public so that the CREATOR can be accessed on cold launch. + * + * @hide + */ + @RestrictTo(LIBRARY) + public static class SavedState extends AbsSavedState { + + Parcelable mLayoutState; + + /** + * called by CREATOR + */ + @SuppressWarnings("deprecation") + SavedState(Parcel in, ClassLoader loader) { + super(in, loader); + mLayoutState = in.readParcelable( + loader != null ? loader : LayoutManager.class.getClassLoader()); } /** - * Smooth scrolls the RecyclerView by a given distance. - * - * @param dx x distance in pixels. - * @param dy y distance in pixels. - * @param duration Duration of the animation in milliseconds. Set to - * {@link #UNDEFINED_DURATION} to have the duration automatically - * calculated - * based on an internally defined standard velocity. - * @param interpolator {@link Interpolator} to be used for scrolling. If it is {@code null}, - * RecyclerView will use an internal default interpolator. + * Called by onSaveInstanceState */ - public void smoothScrollBy(int dx, int dy, int duration, - @Nullable Interpolator interpolator) { + SavedState(Parcelable superState) { + super(superState); + } - // Handle cases where parameter values aren't defined. - if (duration == UNDEFINED_DURATION) { - duration = computeScrollDuration(dx, dy); - } - if (interpolator == null) { - interpolator = sQuinticInterpolator; + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelable(mLayoutState, 0); + } + + void copyFrom(SavedState other) { + mLayoutState = other.mLayoutState; + } + + public static final Creator CREATOR = new ClassLoaderCreator() { + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); } - // If the Interpolator has changed, create a new OverScroller with the new - // interpolator. - if (mInterpolator != interpolator) { - mInterpolator = interpolator; - mOverScroller = new OverScroller(getContext(), interpolator); + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in, null); } - // Reset the last fling information. - mLastFlingX = mLastFlingY = 0; + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } - // Set to settling state and start scrolling. - setScrollState(SCROLL_STATE_SETTLING); - mOverScroller.startScroll(0, 0, dx, dy, duration); + /** + *

    Contains useful information about the current RecyclerView state like target scroll + * position or view focus. State object can also keep arbitrary data, identified by resource + * ids.

    + *

    Often times, RecyclerView components will need to pass information between each other. + * To provide a well defined data bus between components, RecyclerView passes the same State + * object to component callbacks and these components can use it to exchange data.

    + *

    If you implement custom components, you can use State's put/get/remove methods to pass + * data between your components without needing to manage their lifecycles.

    + */ + public static class State { + static final int STEP_START = 1; + static final int STEP_LAYOUT = 1 << 1; + static final int STEP_ANIMATIONS = 1 << 2; - if (Build.VERSION.SDK_INT < 23) { - // b/64931938 before API 23, startScroll() does not reset getCurX()/getCurY() - // to start values, which causes fillRemainingScrollValues() put in obsolete values - // for LayoutManager.onLayoutChildren(). - mOverScroller.computeScrollOffset(); + void assertLayoutStep(int accepted) { + if ((accepted & mLayoutStep) == 0) { + throw new IllegalStateException("Layout state should be one of " + + Integer.toBinaryString(accepted) + " but it is " + + Integer.toBinaryString(mLayoutStep)); } - - postOnAnimation(); } + + /** Owned by SmoothScroller */ + int mTargetPosition = RecyclerView.NO_POSITION; + + private SparseArray mData; + + //////////////////////////////////////////////////////////////////////////////////////////// + // Fields below are carried from one layout pass to the next + //////////////////////////////////////////////////////////////////////////////////////////// + /** - * Computes of an animated scroll in milliseconds. - * - * @param dx x distance in pixels. - * @param dy y distance in pixels. - * @return The duration of the animated scroll in milliseconds. + * Number of items adapter had in the previous layout. */ - private int computeScrollDuration(int dx, int dy) { - int absDx = Math.abs(dx); - int absDy = Math.abs(dy); - boolean horizontal = absDx > absDy; - int containerSize = horizontal ? getWidth() : getHeight(); + int mPreviousLayoutItemCount = 0; - float absDelta = (float) (horizontal ? absDx : absDy); - int duration = (int) (((absDelta / containerSize) + 1) * 300); + /** + * Number of items that were NOT laid out but has been deleted from the adapter after the + * previous layout. + */ + int mDeletedInvisibleItemCountSincePreviousLayout = 0; - return Math.min(duration, MAX_SCROLL_DURATION); - } + //////////////////////////////////////////////////////////////////////////////////////////// + // Fields below must be updated or cleared before they are used (generally before a pass) + //////////////////////////////////////////////////////////////////////////////////////////// - public void stop() { - removeCallbacks(this); - mOverScroller.abortAnimation(); + @IntDef(flag = true, value = { + STEP_START, STEP_LAYOUT, STEP_ANIMATIONS + }) + @Retention(RetentionPolicy.SOURCE) + @interface LayoutState { } - } + @LayoutState + int mLayoutStep = STEP_START; - private class RecyclerViewDataObserver extends AdapterDataObserver { - RecyclerViewDataObserver() { - } + /** + * Number of items adapter has. + */ + int mItemCount = 0; - @Override - public void onChanged() { - assertNotInLayoutOrScroll(null); - mState.mStructureChanged = true; + boolean mStructureChanged = false; - processDataSetCompletelyChanged(true); - if (!mAdapterHelper.hasPendingUpdates()) { - requestLayout(); - } + /** + * True if the associated {@link RecyclerView} is in the pre-layout step where it is having + * its {@link LayoutManager} layout items where they will be at the beginning of a set of + * predictive item animations. + */ + boolean mInPreLayout = false; + + boolean mTrackOldChangeHolders = false; + + boolean mIsMeasuring = false; + + //////////////////////////////////////////////////////////////////////////////////////////// + // Fields below are always reset outside of the pass (or passes) that use them + //////////////////////////////////////////////////////////////////////////////////////////// + + boolean mRunSimpleAnimations = false; + + boolean mRunPredictiveAnimations = false; + + /** + * This data is saved before a layout calculation happens. After the layout is finished, + * if the previously focused view has been replaced with another view for the same item, we + * move the focus to the new item automatically. + */ + int mFocusedItemPosition; + long mFocusedItemId; + // when a sub child has focus, record its id and see if we can directly request focus on + // that one instead + int mFocusedSubChildId; + + int mRemainingScrollHorizontal; + int mRemainingScrollVertical; + + //////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Prepare for a prefetch occurring on the RecyclerView in between traversals, potentially + * prior to any layout passes. + * + *

    Don't touch any state stored between layout passes, only reset per-layout state, so + * that Recycler#getViewForPosition() can function safely.

    + */ + void prepareForNestedPrefetch(Adapter adapter) { + mLayoutStep = STEP_START; + mItemCount = adapter.getItemCount(); + mInPreLayout = false; + mTrackOldChangeHolders = false; + mIsMeasuring = false; + } + + /** + * Returns true if the RecyclerView is currently measuring the layout. This value is + * {@code true} only if the LayoutManager opted into the auto measure API and RecyclerView + * has non-exact measurement specs. + *

    + * Note that if the LayoutManager supports predictive animations and it is calculating the + * pre-layout step, this value will be {@code false} even if the RecyclerView is in + * {@code onMeasure} call. This is because pre-layout means the previous state of the + * RecyclerView and measurements made for that state cannot change the RecyclerView's size. + * LayoutManager is always guaranteed to receive another call to + * {@link LayoutManager#onLayoutChildren(Recycler, State)} when this happens. + * + * @return True if the RecyclerView is currently calculating its bounds, false otherwise. + */ + public boolean isMeasuring() { + return mIsMeasuring; } - @Override - public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { - assertNotInLayoutOrScroll(null); - if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) { - triggerUpdateProcessor(); - } + /** + * Returns true if the {@link RecyclerView} is in the pre-layout step where it is having its + * {@link LayoutManager} layout items where they will be at the beginning of a set of + * predictive item animations. + */ + public boolean isPreLayout() { + return mInPreLayout; } - @Override - public void onItemRangeInserted(int positionStart, int itemCount) { - assertNotInLayoutOrScroll(null); - if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) { - triggerUpdateProcessor(); - } + /** + * Returns whether RecyclerView will run predictive animations in this layout pass + * or not. + * + * @return true if RecyclerView is calculating predictive animations to be run at the end + * of the layout pass. + */ + public boolean willRunPredictiveAnimations() { + return mRunPredictiveAnimations; } - @Override - public void onItemRangeRemoved(int positionStart, int itemCount) { - assertNotInLayoutOrScroll(null); - if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) { - triggerUpdateProcessor(); - } + /** + * Returns whether RecyclerView will run simple animations in this layout pass + * or not. + * + * @return true if RecyclerView is calculating simple animations to be run at the end of + * the layout pass. + */ + public boolean willRunSimpleAnimations() { + return mRunSimpleAnimations; } - @Override - public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { - assertNotInLayoutOrScroll(null); - if (mAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) { - triggerUpdateProcessor(); + /** + * Removes the mapping from the specified id, if there was any. + * + * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.* to + * preserve cross functionality and avoid conflicts. + */ + public void remove(int resourceId) { + if (mData == null) { + return; } + mData.remove(resourceId); } - void triggerUpdateProcessor() { - if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) { - ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable); - } else { - mAdapterUpdateDuringMeasure = true; - requestLayout(); + /** + * Gets the Object mapped from the specified id, or null + * if no such data exists. + * + * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.* + * to + * preserve cross functionality and avoid conflicts. + */ + @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"}) + public T get(int resourceId) { + if (mData == null) { + return null; } + return (T) mData.get(resourceId); } - @Override - public void onStateRestorationPolicyChanged() { - if (mPendingSavedState == null) { - return; - } - // If there is a pending saved state and the new mode requires us to restore it, - // we'll request a layout which will call the adapter to see if it can restore state - // and trigger state restoration - Adapter adapter = mAdapter; - if (adapter != null && adapter.canRestoreState()) { - requestLayout(); + /** + * Adds a mapping from the specified id to the specified value, replacing the previous + * mapping from the specified key if there was one. + * + * @param resourceId Id of the resource you want to add. It is suggested to use R.id.* to + * preserve cross functionality and avoid conflicts. + * @param data The data you want to associate with the resourceId. + */ + public void put(int resourceId, Object data) { + if (mData == null) { + mData = new SparseArray<>(); } + mData.put(resourceId, data); } - } - - /** - * A Recycler is responsible for managing scrapped or detached item views for reuse. - * - *

    A "scrapped" view is a view that is still attached to its parent RecyclerView but - * that has been marked for removal or reuse.

    - * - *

    Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for - * an adapter's data set representing the data at a given position or item ID. - * If the view to be reused is considered "dirty" the adapter will be asked to rebind it. - * If not, the view can be quickly reused by the LayoutManager with no further work. - * Clean views that have not {@link android.view.View#isLayoutRequested() requested layout} - * may be repositioned by a LayoutManager without remeasurement.

    - */ - public final class Recycler { - static final int DEFAULT_CACHE_SIZE = 2; - final ArrayList mAttachedScrap = new ArrayList<>(); - final ArrayList mCachedViews = new ArrayList<>(); - - private final List - mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap); - ArrayList mChangedScrap; - int mViewCacheMax = DEFAULT_CACHE_SIZE; - - RecycledViewPool mRecyclerPool; - private int mRequestedCacheMax = DEFAULT_CACHE_SIZE; - private ViewCacheExtension mViewCacheExtension; /** - * Clear scrap views out of this recycler. Detached views contained within a - * recycled view pool will remain. + * If scroll is triggered to make a certain item visible, this value will return the + * adapter index of that item. + * + * @return Adapter index of the target item or + * {@link RecyclerView#NO_POSITION} if there is no target + * position. */ - public void clear() { - mAttachedScrap.clear(); - recycleAndClearCachedViews(); + public int getTargetScrollPosition() { + return mTargetPosition; } /** - * Set the maximum number of detached, valid views we should retain for later use. + * Returns if current scroll has a target position. * - * @param viewCount Number of views to keep before sending views to the shared pool + * @return true if scroll is being triggered to make a certain position visible + * @see #getTargetScrollPosition() */ - public void setViewCacheSize(int viewCount) { - mRequestedCacheMax = viewCount; - updateViewCacheSize(); + public boolean hasTargetScrollPosition() { + return mTargetPosition != RecyclerView.NO_POSITION; } - void updateViewCacheSize() { - int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0; - mViewCacheMax = mRequestedCacheMax + extraCache; + /** + * @return true if the structure of the data set has changed since the last call to + * onLayoutChildren, false otherwise + */ + public boolean didStructureChange() { + return mStructureChanged; + } - // first, try the views that can be recycled - for (int i = mCachedViews.size() - 1; - i >= 0 && mCachedViews.size() > mViewCacheMax; i--) { - recycleCachedViewAt(i); - } + /** + * Returns the total number of items that can be laid out. Note that this number is not + * necessarily equal to the number of items in the adapter, so you should always use this + * number for your position calculations and never access the adapter directly. + *

    + * RecyclerView listens for Adapter's notify events and calculates the effects of adapter + * data changes on existing Views. These calculations are used to decide which animations + * should be run. + *

    + * To support predictive animations, RecyclerView may rewrite or reorder Adapter changes to + * present the correct state to LayoutManager in pre-layout pass. + *

    + * For example, a newly added item is not included in pre-layout item count because + * pre-layout reflects the contents of the adapter before the item is added. Behind the + * scenes, RecyclerView offsets {@link Recycler#getViewForPosition(int)} calls such that + * LayoutManager does not know about the new item's existence in pre-layout. The item will + * be available in second layout pass and will be included in the item count. Similar + * adjustments are made for moved and removed items as well. + *

    + * You can get the adapter's item count via {@link LayoutManager#getItemCount()} method. + * + * @return The number of items currently available + * @see LayoutManager#getItemCount() + */ + public int getItemCount() { + return mInPreLayout + ? (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout) + : mItemCount; } /** - * Returns an unmodifiable list of ViewHolders that are currently in the scrap list. + * Returns remaining horizontal scroll distance of an ongoing scroll animation(fling/ + * smoothScrollTo/SmoothScroller) in pixels. Returns zero if {@link #getScrollState()} is + * other than {@link #SCROLL_STATE_SETTLING}. * - * @return List of ViewHolders in the scrap list. + * @return Remaining horizontal scroll distance */ - @NonNull - public List getScrapList() { - return mUnmodifiableAttachedScrap; + public int getRemainingScrollHorizontal() { + return mRemainingScrollHorizontal; } /** - * Helper method for getViewForPosition. - *

    - * Checks whether a given view holder can be used for the provided position. + * Returns remaining vertical scroll distance of an ongoing scroll animation(fling/ + * smoothScrollTo/SmoothScroller) in pixels. Returns zero if {@link #getScrollState()} is + * other than {@link #SCROLL_STATE_SETTLING}. * - * @param holder ViewHolder - * @return true if ViewHolder matches the provided position, false otherwise + * @return Remaining vertical scroll distance */ - boolean validateViewHolderForOffsetPosition(ViewHolder holder) { - // if it is a removed holder, nothing to verify since we cannot ask adapter anymore - // if it is not removed, verify the type and id. - if (holder.isRemoved()) { - if (DEBUG && !mState.isPreLayout()) { - throw new IllegalStateException("should not receive a removed view unless it" - + " is pre layout" + exceptionLabel()); - } - return mState.isPreLayout(); - } - if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) { - throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder " - + "adapter position" + holder + exceptionLabel()); - } - if (!mState.isPreLayout()) { - // don't check type if it is pre-layout. - int type = mAdapter.getItemViewType(holder.mPosition); - if (type != holder.getItemViewType()) { - return false; - } - } - if (mAdapter.hasStableIds()) { - return holder.getItemId() == mAdapter.getItemId(holder.mPosition); - } - return true; + public int getRemainingScrollVertical() { + return mRemainingScrollVertical; + } + + @Override + public String toString() { + return "State{" + + "mTargetPosition=" + mTargetPosition + + ", mData=" + mData + + ", mItemCount=" + mItemCount + + ", mIsMeasuring=" + mIsMeasuring + + ", mPreviousLayoutItemCount=" + mPreviousLayoutItemCount + + ", mDeletedInvisibleItemCountSincePreviousLayout=" + + mDeletedInvisibleItemCountSincePreviousLayout + + ", mStructureChanged=" + mStructureChanged + + ", mInPreLayout=" + mInPreLayout + + ", mRunSimpleAnimations=" + mRunSimpleAnimations + + ", mRunPredictiveAnimations=" + mRunPredictiveAnimations + + '}'; } + } + + /** + * This class defines the behavior of fling if the developer wishes to handle it. + *

    + * Subclasses of {@link OnFlingListener} can be used to implement custom fling behavior. + * + * @see #setOnFlingListener(OnFlingListener) + */ + public abstract static class OnFlingListener { /** - * Attempts to bind view, and account for relevant timing information. If - * deadlineNs != FOREVER_NS, this method may fail to bind, and return false. + * Override this to handle a fling given the velocities in both x and y directions. + * Note that this method will only be called if the associated {@link LayoutManager} + * supports scrolling and the fling is not handled by nested scrolls first. * - * @param holder Holder to be bound. - * @param offsetPosition Position of item to be bound. - * @param position Pre-layout position of item to be bound. - * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should - * complete. If FOREVER_NS is passed, this method will not fail to - * bind the holder. + * @param velocityX the fling velocity on the X axis + * @param velocityY the fling velocity on the Y axis + * @return true if the fling was handled, false otherwise. */ - @SuppressWarnings("unchecked") - private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition, - int position, long deadlineNs) { - holder.mBindingAdapter = null; - holder.mOwnerRecyclerView = RecyclerView.this; - int viewType = holder.getItemViewType(); - long startBindNs = getNanoTime(); - if (deadlineNs != FOREVER_NS - && !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) { - // abort - we have a deadline we can't meet - return false; + public abstract boolean onFling(int velocityX, int velocityY); + } + + /** + * Internal listener that manages items after animations finish. This is how items are + * retained (not recycled) during animations, but allowed to be recycled afterwards. + * It depends on the contract with the ItemAnimator to call the appropriate dispatch*Finished() + * method on the animator's listener when it is done animating any item. + */ + private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener { + + ItemAnimatorRestoreListener() { + } + + @Override + public void onAnimationFinished(ViewHolder item) { + item.setIsRecyclable(true); + if (item.mShadowedHolder != null && item.mShadowingHolder == null) { // old vh + item.mShadowedHolder = null; } - mAdapter.bindViewHolder(holder, offsetPosition); - long endBindNs = getNanoTime(); - mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs); - attachAccessibilityDelegateOnBind(holder); - if (mState.isPreLayout()) { - holder.mPreLayoutPosition = position; + // always null this because an OldViewHolder can never become NewViewHolder w/o being + // recycled. + item.mShadowingHolder = null; + if (!item.shouldBeKeptAsChild()) { + if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) { + removeDetachedView(item.itemView, false); + } } - return true; } + } + + /** + * This class defines the animations that take place on items as changes are made + * to the adapter. + * + * Subclasses of ItemAnimator can be used to implement custom animations for actions on + * ViewHolder items. The RecyclerView will manage retaining these items while they + * are being animated, but implementors must call {@link #dispatchAnimationFinished(ViewHolder)} + * when a ViewHolder's animation is finished. In other words, there must be a matching + * {@link #dispatchAnimationFinished(ViewHolder)} call for each + * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) animateAppearance()}, + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange()} + * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) animatePersistence()}, + * and + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance()} call. + * + *

    By default, RecyclerView uses {@link DefaultItemAnimator}.

    + * + * @see #setItemAnimator(ItemAnimator) + */ + @SuppressWarnings("UnusedParameters") + public abstract static class ItemAnimator { /** - * Binds the given View to the position. The View can be a View previously retrieved via - * {@link #getViewForPosition(int)} or created by - * {@link Adapter#onCreateViewHolder(ViewGroup, int)}. - *

    - * Generally, a LayoutManager should acquire its views via {@link #getViewForPosition(int)} - * and let the RecyclerView handle caching. This is a helper method for LayoutManager who - * wants to handle its own recycling logic. + * The Item represented by this ViewHolder is updated. *

    - * Note that, {@link #getViewForPosition(int)} already binds the View to the position so - * you don't need to call this method unless you want to bind this View to another position. * - * @param view The view to update. - * @param position The position of the item to bind to this View. + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) */ - public void bindViewToPosition(@NonNull View view, int position) { - ViewHolder holder = getChildViewHolderInt(view); - if (holder == null) { - throw new IllegalArgumentException("The view does not have a ViewHolder. You cannot" - + " pass arbitrary views to this method, they should be created by the " - + "Adapter" + exceptionLabel()); - } - int offsetPosition = mAdapterHelper.findPositionOffset(position); - if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { - throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " - + "position " + position + "(offset:" + offsetPosition + ")." - + "state:" + mState.getItemCount() + exceptionLabel()); - } - tryBindViewHolderByDeadline(holder, offsetPosition, position, FOREVER_NS); - - ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); - LayoutParams rvLayoutParams; - if (lp == null) { - rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); - holder.itemView.setLayoutParams(rvLayoutParams); - } else if (!checkLayoutParams(lp)) { - rvLayoutParams = (LayoutParams) generateLayoutParams(lp); - holder.itemView.setLayoutParams(rvLayoutParams); - } else { - rvLayoutParams = (LayoutParams) lp; - } - - rvLayoutParams.mInsetsDirty = true; - rvLayoutParams.mViewHolder = holder; - rvLayoutParams.mPendingInvalidate = holder.itemView.getParent() == null; - } + public static final int FLAG_CHANGED = ViewHolder.FLAG_UPDATE; /** - * RecyclerView provides artificial position range (item count) in pre-layout state and - * automatically maps these positions to {@link Adapter} positions when - * {@link #getViewForPosition(int)} or {@link #bindViewToPosition(View, int)} is called. - *

    - * Usually, LayoutManager does not need to worry about this. However, in some cases, your - * LayoutManager may need to call some custom component with item positions in which - * case you need the actual adapter position instead of the pre layout position. You - * can use this method to convert a pre-layout position to adapter (post layout) position. - *

    - * Note that if the provided position belongs to a deleted ViewHolder, this method will - * return -1. + * The Item represented by this ViewHolder is removed from the adapter. *

    - * Calling this method in post-layout state returns the same value back. * - * @param position The pre-layout position to convert. Must be greater or equal to 0 and - * less than {@link State#getItemCount()}. + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) */ - public int convertPreLayoutPositionToPostLayout(int position) { - if (position < 0 || position >= mState.getItemCount()) { - throw new IndexOutOfBoundsException("invalid position " + position + ". State " - + "item count is " + mState.getItemCount() + exceptionLabel()); - } - if (!mState.isPreLayout()) { - return position; - } - return mAdapterHelper.findPositionOffset(position); - } + public static final int FLAG_REMOVED = ViewHolder.FLAG_REMOVED; /** - * Obtain a view initialized for the given position. - *

    - * This method should be used by {@link LayoutManager} implementations to obtain - * views to represent data from an {@link Adapter}. + * Adapter {@link Adapter#notifyDataSetChanged()} has been called and the content + * represented by this ViewHolder is invalid. *

    - * The Recycler may reuse a scrap or detached view from a shared pool if one is - * available for the correct view type. If the adapter has not indicated that the - * data at the given position has changed, the Recycler will attempt to hand back - * a scrap view that was previously initialized for that data without rebinding. * - * @param position Position to obtain a view for - * @return A view representing the data at position from adapter + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) */ - @NonNull - public View getViewForPosition(int position) { - return getViewForPosition(position, false); - } - - View getViewForPosition(int position, boolean dryRun) { - return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; - } + public static final int FLAG_INVALIDATED = ViewHolder.FLAG_INVALID; /** - * Attempts to get the ViewHolder for the given position, either from the Recycler scrap, - * cache, the RecycledViewPool, or creating it directly. + * The position of the Item represented by this ViewHolder has been changed. This flag is + * not bound to {@link Adapter#notifyItemMoved(int, int)}. It might be set in response to + * any adapter change that may have a side effect on this item. (e.g. The item before this + * one has been removed from the Adapter). *

    - * If a deadlineNs other than {@link #FOREVER_NS} is passed, this method early return - * rather than constructing or binding a ViewHolder if it doesn't think it has time. - * If a ViewHolder must be constructed and not enough time remains, null is returned. If a - * ViewHolder is aquired and must be bound but not enough time remains, an unbound holder is - * returned. Use {@link ViewHolder#isBound()} on the returned object to check for this. * - * @param position Position of ViewHolder to be returned. - * @param dryRun True if the ViewHolder should not be removed from scrap/cache/ - * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should - * complete. If FOREVER_NS is passed, this method will not fail to - * create/bind the holder if needed. - * @return ViewHolder for requested position + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) */ - @Nullable - ViewHolder tryGetViewHolderForPositionByDeadline(int position, - boolean dryRun, long deadlineNs) { - if (position < 0 || position >= mState.getItemCount()) { - throw new IndexOutOfBoundsException("Invalid item position " + position - + "(" + position + "). Item count:" + mState.getItemCount() - + exceptionLabel()); - } - boolean fromScrapOrHiddenOrCache = false; - ViewHolder holder = null; - // 0) If there is a changed scrap, try to find from there - if (mState.isPreLayout()) { - holder = getChangedScrapViewForPosition(position); - fromScrapOrHiddenOrCache = holder != null; - } - // 1) Find by position from scrap/hidden list/cache - if (holder == null) { - holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); - if (holder != null) { - if (!validateViewHolderForOffsetPosition(holder)) { - // recycle holder (and unscrap if relevant) since it can't be used - if (!dryRun) { - // we would like to recycle this but need to make sure it is not used by - // animation logic etc. - holder.addFlags(ViewHolder.FLAG_INVALID); - if (holder.isScrap()) { - removeDetachedView(holder.itemView, false); - holder.unScrap(); - } else if (holder.wasReturnedFromScrap()) { - holder.clearReturnedFromScrapFlag(); - } - recycleViewHolderInternal(holder); - } - holder = null; - } else { - fromScrapOrHiddenOrCache = true; - } - } - } - if (holder == null) { - int offsetPosition = mAdapterHelper.findPositionOffset(position); - if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { - throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " - + "position " + position + "(offset:" + offsetPosition + ")." - + "state:" + mState.getItemCount() + exceptionLabel()); - } + public static final int FLAG_MOVED = ViewHolder.FLAG_MOVED; - int type = mAdapter.getItemViewType(offsetPosition); - // 2) Find from scrap/cache via stable ids, if exists - if (mAdapter.hasStableIds()) { - holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), - type, dryRun); - if (holder != null) { - // update position - holder.mPosition = offsetPosition; - fromScrapOrHiddenOrCache = true; - } - } - if (holder == null && mViewCacheExtension != null) { - // We are NOT sending the offsetPosition because LayoutManager does not - // know it. - View view = mViewCacheExtension - .getViewForPositionAndType(this, position, type); - if (view != null) { - holder = getChildViewHolder(view); - if (holder == null) { - throw new IllegalArgumentException("getViewForPositionAndType returned" - + " a view which does not have a ViewHolder" - + exceptionLabel()); - } else if (holder.shouldIgnore()) { - throw new IllegalArgumentException("getViewForPositionAndType returned" - + " a view that is ignored. You must call stopIgnoring before" - + " returning this view." + exceptionLabel()); - } - } - } - if (holder == null) { // fallback to pool - if (DEBUG) { - Log.d(TAG, "tryGetViewHolderForPositionByDeadline(" - + position + ") fetching from shared pool"); - } - holder = getRecycledViewPool().getRecycledView(type); - if (holder != null) { - holder.resetInternal(); - if (FORCE_INVALIDATE_DISPLAY_LIST) { - invalidateDisplayListInt(holder); - } - } - } - if (holder == null) { - long start = getNanoTime(); - if (deadlineNs != FOREVER_NS - && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) { - // abort - we have a deadline we can't meet - return null; - } - holder = mAdapter.createViewHolder(RecyclerView.this, type); - if (ALLOW_THREAD_GAP_WORK) { - // only bother finding nested RV if prefetching - RecyclerView innerView = findNestedRecyclerView(holder.itemView); - if (innerView != null) { - holder.mNestedRecyclerView = new WeakReference<>(innerView); - } - } + /** + * This ViewHolder was not laid out but has been added to the layout in pre-layout state + * by the {@link LayoutManager}. This means that the item was already in the Adapter but + * invisible and it may become visible in the post layout phase. LayoutManagers may prefer + * to add new items in pre-layout to specify their virtual location when they are invisible + * (e.g. to specify the item should animate in from below the visible area). + *

    + * + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + */ + public static final int FLAG_APPEARED_IN_PRE_LAYOUT = + ViewHolder.FLAG_APPEARED_IN_PRE_LAYOUT; - long end = getNanoTime(); - mRecyclerPool.factorInCreateTime(type, end - start); - if (DEBUG) { - Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder"); - } - } - } + /** + * The set of flags that might be passed to + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + */ + @IntDef(flag = true, value = { + FLAG_CHANGED, FLAG_REMOVED, FLAG_MOVED, FLAG_INVALIDATED, + FLAG_APPEARED_IN_PRE_LAYOUT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AdapterChanges { + } - // This is very ugly but the only place we can grab this information - // before the View is rebound and returned to the LayoutManager for post layout ops. - // We don't need this in pre-layout since the VH is not updated by the LM. - if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder - .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) { - holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); - if (mState.mRunSimpleAnimations) { - int changeFlags = ItemAnimator - .buildAdapterChangeFlagsForAnimations(holder); - changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; - ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState, - holder, changeFlags, holder.getUnmodifiedPayloads()); - recordAnimationInfoIfBouncedHiddenView(holder, info); - } - } + private ItemAnimatorListener mListener = null; + private ArrayList mFinishedListeners = + new ArrayList<>(); - boolean bound = false; - if (mState.isPreLayout() && holder.isBound()) { - // do not update unless we absolutely have to. - holder.mPreLayoutPosition = position; - } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { - if (DEBUG && holder.isRemoved()) { - throw new IllegalStateException("Removed holder should be bound and it should" - + " come here only in pre-layout. Holder: " + holder - + exceptionLabel()); - } - int offsetPosition = mAdapterHelper.findPositionOffset(position); - bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); - } + private long mAddDuration = 120; + private long mRemoveDuration = 120; + private long mMoveDuration = 250; + private long mChangeDuration = 250; - ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); - LayoutParams rvLayoutParams; - if (lp == null) { - rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); - holder.itemView.setLayoutParams(rvLayoutParams); - } else if (!checkLayoutParams(lp)) { - rvLayoutParams = (LayoutParams) generateLayoutParams(lp); - holder.itemView.setLayoutParams(rvLayoutParams); - } else { - rvLayoutParams = (LayoutParams) lp; - } - rvLayoutParams.mViewHolder = holder; - rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound; - return holder; + /** + * Gets the current duration for which all move animations will run. + * + * @return The current move duration + */ + public long getMoveDuration() { + return mMoveDuration; } - private void attachAccessibilityDelegateOnBind(ViewHolder holder) { - if (isAccessibilityEnabled()) { - View itemView = holder.itemView; - if (ViewCompat.getImportantForAccessibility(itemView) - == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - ViewCompat.setImportantForAccessibility(itemView, - ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); - } - if (mAccessibilityDelegate == null) { - return; - } - AccessibilityDelegateCompat itemDelegate = mAccessibilityDelegate.getItemDelegate(); - if (itemDelegate instanceof RecyclerViewAccessibilityDelegate.ItemDelegate) { - // If there was already an a11y delegate set on the itemView, store it in the - // itemDelegate and then set the itemDelegate as the a11y delegate. - ((RecyclerViewAccessibilityDelegate.ItemDelegate) itemDelegate) - .saveOriginalDelegate(itemView); - } - ViewCompat.setAccessibilityDelegate(itemView, itemDelegate); - } + /** + * Sets the duration for which all move animations will run. + * + * @param moveDuration The move duration + */ + public void setMoveDuration(long moveDuration) { + mMoveDuration = moveDuration; } - private void invalidateDisplayListInt(ViewHolder holder) { - if (holder.itemView instanceof ViewGroup) { - invalidateDisplayListInt((ViewGroup) holder.itemView, false); - } + /** + * Gets the current duration for which all add animations will run. + * + * @return The current add duration + */ + public long getAddDuration() { + return mAddDuration; } - private void invalidateDisplayListInt(ViewGroup viewGroup, boolean invalidateThis) { - for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) { - View view = viewGroup.getChildAt(i); - if (view instanceof ViewGroup) { - invalidateDisplayListInt((ViewGroup) view, true); - } - } - if (!invalidateThis) { - return; - } - // we need to force it to become invisible - if (viewGroup.getVisibility() == View.INVISIBLE) { - viewGroup.setVisibility(View.VISIBLE); - viewGroup.setVisibility(View.INVISIBLE); - } else { - int visibility = viewGroup.getVisibility(); - viewGroup.setVisibility(View.INVISIBLE); - viewGroup.setVisibility(visibility); - } + /** + * Sets the duration for which all add animations will run. + * + * @param addDuration The add duration + */ + public void setAddDuration(long addDuration) { + mAddDuration = addDuration; } /** - * Recycle a detached view. The specified view will be added to a pool of views - * for later rebinding and reuse. + * Gets the current duration for which all remove animations will run. * - *

    A view must be fully detached (removed from parent) before it may be recycled. If the - * View is scrapped, it will be removed from scrap list.

    + * @return The current remove duration + */ + public long getRemoveDuration() { + return mRemoveDuration; + } + + /** + * Sets the duration for which all remove animations will run. * - * @param view Removed view for recycling - * @see LayoutManager#removeAndRecycleView(View, Recycler) + * @param removeDuration The remove duration */ - public void recycleView(@NonNull View view) { - // This public recycle method tries to make view recycle-able since layout manager - // intended to recycle this view (e.g. even if it is in scrap or change cache) - ViewHolder holder = getChildViewHolderInt(view); - if (holder.isTmpDetached()) { - removeDetachedView(view, false); - } - if (holder.isScrap()) { - holder.unScrap(); - } else if (holder.wasReturnedFromScrap()) { - holder.clearReturnedFromScrapFlag(); - } - recycleViewHolderInternal(holder); - // In most cases we dont need call endAnimation() because when view is detached, - // ViewPropertyAnimation will end. But if the animation is based on ObjectAnimator or - // if the ItemAnimator uses "pending runnable" and the ViewPropertyAnimation has not - // started yet, the ItemAnimatior on the view may not be cleared. - // In b/73552923, the View is removed by scroll pass while it's waiting in - // the "pending moving" list of DefaultItemAnimator and DefaultItemAnimator later in - // a post runnable, incorrectly performs postDelayed() on the detached view. - // To fix the issue, we issue endAnimation() here to make sure animation of this view - // finishes. - // - // Note the order: we must call endAnimation() after recycleViewHolderInternal() - // to avoid recycle twice. If ViewHolder isRecyclable is false, - // recycleViewHolderInternal() will not recycle it, endAnimation() will reset - // isRecyclable flag and recycle the view. - if (mItemAnimator != null && !holder.isRecyclable()) { - mItemAnimator.endAnimation(holder); - } + public void setRemoveDuration(long removeDuration) { + mRemoveDuration = removeDuration; } - void recycleAndClearCachedViews() { - int count = mCachedViews.size(); - for (int i = count - 1; i >= 0; i--) { - recycleCachedViewAt(i); - } - mCachedViews.clear(); - if (ALLOW_THREAD_GAP_WORK) { - mPrefetchRegistry.clearPrefetchPositions(); - } + /** + * Gets the current duration for which all change animations will run. + * + * @return The current change duration + */ + public long getChangeDuration() { + return mChangeDuration; } /** - * Recycles a cached view and removes the view from the list. Views are added to cache - * if and only if they are recyclable, so this method does not check it again. - *

    - * A small exception to this rule is when the view does not have an animator reference - * but transient state is true (due to animations created outside ItemAnimator). In that - * case, adapter may choose to recycle it. From RecyclerView's perspective, the view is - * still recyclable since Adapter wants to do so. + * Sets the duration for which all change animations will run. * - * @param cachedViewIndex The index of the view in cached views list + * @param changeDuration The change duration */ - void recycleCachedViewAt(int cachedViewIndex) { - if (DEBUG) { - Log.d(TAG, "Recycling cached view at index " + cachedViewIndex); - } - ViewHolder viewHolder = mCachedViews.get(cachedViewIndex); - if (DEBUG) { - Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder); - } - addViewHolderToRecycledViewPool(viewHolder, true); - mCachedViews.remove(cachedViewIndex); + public void setChangeDuration(long changeDuration) { + mChangeDuration = changeDuration; } /** - * internal implementation checks if view is scrapped or attached and throws an exception - * if so. - * Public version un-scraps before calling recycle. + * Internal only: + * Sets the listener that must be called when the animator is finished + * animating the item (or immediately if no animation happens). This is set + * internally and is not intended to be set by external code. + * + * @param listener The listener that must be called. */ - void recycleViewHolderInternal(ViewHolder holder) { - if (holder.isScrap() || holder.itemView.getParent() != null) { - throw new IllegalArgumentException( - "Scrapped or attached views may not be recycled. isScrap:" - + holder.isScrap() + " isAttached:" - + (holder.itemView.getParent() != null) + exceptionLabel()); - } - - if (holder.isTmpDetached()) { - throw new IllegalArgumentException("Tmp detached view should be removed " - + "from RecyclerView before it can be recycled: " + holder - + exceptionLabel()); - } - - if (holder.shouldIgnore()) { - throw new IllegalArgumentException("Trying to recycle an ignored view holder. You" - + " should first call stopIgnoringView(view) before calling recycle." - + exceptionLabel()); - } - boolean transientStatePreventsRecycling = holder - .doesTransientStatePreventRecycling(); - @SuppressWarnings("unchecked") boolean forceRecycle = mAdapter != null - && transientStatePreventsRecycling - && mAdapter.onFailedToRecycleView(holder); - boolean cached = false; - boolean recycled = false; - if (DEBUG && mCachedViews.contains(holder)) { - throw new IllegalArgumentException("cached view received recycle internal? " - + holder + exceptionLabel()); - } - if (forceRecycle || holder.isRecyclable()) { - if (mViewCacheMax > 0 - && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID - | ViewHolder.FLAG_REMOVED - | ViewHolder.FLAG_UPDATE - | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { - // Retire oldest cached view - int cachedViewSize = mCachedViews.size(); - if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { - recycleCachedViewAt(0); - cachedViewSize--; - } - - int targetCacheIndex = cachedViewSize; - if (ALLOW_THREAD_GAP_WORK - && cachedViewSize > 0 - && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) { - // when adding the view, skip past most recently prefetched views - int cacheIndex = cachedViewSize - 1; - while (cacheIndex >= 0) { - int cachedPos = mCachedViews.get(cacheIndex).mPosition; - if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) { - break; - } - cacheIndex--; - } - targetCacheIndex = cacheIndex + 1; - } - mCachedViews.add(targetCacheIndex, holder); - cached = true; - } - if (!cached) { - addViewHolderToRecycledViewPool(holder, true); - recycled = true; - } - } else { - // NOTE: A view can fail to be recycled when it is scrolled off while an animation - // runs. In this case, the item is eventually recycled by - // ItemAnimatorRestoreListener#onAnimationFinished. - - // TODO: consider cancelling an animation when an item is removed scrollBy, - // to return it to the pool faster - if (DEBUG) { - Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will " - + "re-visit here. We are still removing it from animation lists" - + exceptionLabel()); - } - } - // even if the holder is not removed, we still call this method so that it is removed - // from view holder lists. - mViewInfoStore.removeViewHolder(holder); - if (!cached && !recycled && transientStatePreventsRecycling) { - PoolingContainer.callPoolingContainerOnRelease(holder.itemView); - holder.mBindingAdapter = null; - holder.mOwnerRecyclerView = null; - } + void setListener(ItemAnimatorListener listener) { + mListener = listener; } /** - * Prepares the ViewHolder to be removed/recycled, and inserts it into the RecycledViewPool. + * Called by the RecyclerView before the layout begins. Item animator should record + * necessary information about the View before it is potentially rebound, moved or removed. *

    - * Pass false to dispatchRecycled for views that have not been bound. + * The data returned from this method will be passed to the related animate** + * methods. + *

    + * Note that this method may be called after pre-layout phase if LayoutManager adds new + * Views to the layout in pre-layout pass. + *

    + * The default implementation returns an {@link ItemHolderInfo} which holds the bounds of + * the View and the adapter change flags. * - * @param holder Holder to be added to the pool. - * @param dispatchRecycled True to dispatch View recycled callbacks. + * @param state The current State of RecyclerView which includes some useful data + * about the layout that will be calculated. + * @param viewHolder The ViewHolder whose information should be recorded. + * @param changeFlags Additional information about what changes happened in the Adapter + * about the Item represented by this ViewHolder. For instance, if + * item is deleted from the adapter, {@link #FLAG_REMOVED} will be set. + * @param payloads The payload list that was previously passed to + * {@link Adapter#notifyItemChanged(int, Object)} or + * {@link Adapter#notifyItemRangeChanged(int, int, Object)}. + * @return An ItemHolderInfo instance that preserves necessary information about the + * ViewHolder. This object will be passed back to related animate** methods + * after layout is complete. + * @see #recordPostLayoutInformation(State, ViewHolder) + * @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) */ - void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) { - clearNestedRecyclerViewIfNotNested(holder); - View itemView = holder.itemView; - if (mAccessibilityDelegate != null) { - AccessibilityDelegateCompat itemDelegate = mAccessibilityDelegate.getItemDelegate(); - AccessibilityDelegateCompat originalDelegate = null; - if (itemDelegate instanceof RecyclerViewAccessibilityDelegate.ItemDelegate) { - originalDelegate = - ((RecyclerViewAccessibilityDelegate.ItemDelegate) itemDelegate) - .getAndRemoveOriginalDelegateForItem(itemView); - } - // Set the a11y delegate back to whatever the original delegate was. - ViewCompat.setAccessibilityDelegate(itemView, originalDelegate); - } - if (dispatchRecycled) { - dispatchViewRecycled(holder); - } - holder.mBindingAdapter = null; - holder.mOwnerRecyclerView = null; - getRecycledViewPool().putRecycledView(holder); + public @NonNull + ItemHolderInfo recordPreLayoutInformation(@NonNull State state, + @NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags, + @NonNull List payloads) { + return obtainHolderInfo().setFrom(viewHolder); } /** - * Used as a fast path for unscrapping and recycling a view during a bulk operation. - * The caller must call {@link #clearScrap()} when it's done to update the recycler's - * internal bookkeeping. + * Called by the RecyclerView after the layout is complete. Item animator should record + * necessary information about the View's final state. + *

    + * The data returned from this method will be passed to the related animate** + * methods. + *

    + * The default implementation returns an {@link ItemHolderInfo} which holds the bounds of + * the View. + * + * @param state The current State of RecyclerView which includes some useful data about + * the layout that will be calculated. + * @param viewHolder The ViewHolder whose information should be recorded. + * @return An ItemHolderInfo that preserves necessary information about the ViewHolder. + * This object will be passed back to related animate** methods when + * RecyclerView decides how items should be animated. + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + * @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) */ - void quickRecycleScrapView(View view) { - ViewHolder holder = getChildViewHolderInt(view); - holder.mScrapContainer = null; - holder.mInChangeScrap = false; - holder.clearReturnedFromScrapFlag(); - recycleViewHolderInternal(holder); + public @NonNull + ItemHolderInfo recordPostLayoutInformation(@NonNull State state, + @NonNull ViewHolder viewHolder) { + return obtainHolderInfo().setFrom(viewHolder); } /** - * Mark an attached view as scrap. + * Called by the RecyclerView when a ViewHolder has disappeared from the layout. + *

    + * This means that the View was a child of the LayoutManager when layout started but has + * been removed by the LayoutManager. It might have been removed from the adapter or simply + * become invisible due to other factors. You can distinguish these two cases by checking + * the change flags that were passed to + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + *

    + * Note that when a ViewHolder both changes and disappears in the same layout pass, the + * animation callback method which will be called by the RecyclerView depends on the + * ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the + * LayoutManager's decision whether to layout the changed version of a disappearing + * ViewHolder or not. RecyclerView will call + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange} instead of {@code animateDisappearance} if and only if the ItemAnimator + * returns {@code false} from + * {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the + * LayoutManager lays out a new disappearing view that holds the updated information. + * Built-in LayoutManagers try to avoid laying out updated versions of disappearing views. + *

    + * If LayoutManager supports predictive animations, it might provide a target disappear + * location for the View by laying it out in that location. When that happens, + * RecyclerView will call {@link #recordPostLayoutInformation(State, ViewHolder)} and the + * response of that call will be passed to this method as the postLayoutInfo. + *

    + * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation + * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it + * decides not to animate the view). * - *

    "Scrap" views are still attached to their parent RecyclerView but are eligible - * for rebinding and reuse. Requests for a view for a given position may return a - * reused or rebound scrap view instance.

    + * @param viewHolder The ViewHolder which should be animated + * @param preLayoutInfo The information that was returned from + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @param postLayoutInfo The information that was returned from + * {@link #recordPostLayoutInformation(State, ViewHolder)}. Might be + * null if the LayoutManager did not layout the item. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animateDisappearance(@NonNull ViewHolder viewHolder, + @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo); + + /** + * Called by the RecyclerView when a ViewHolder is added to the layout. + *

    + * In detail, this means that the ViewHolder was not a child when the layout started + * but has been added by the LayoutManager. It might be newly added to the adapter or + * simply become visible due to other factors. + *

    + * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation + * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it + * decides not to animate the view). * - * @param view View to scrap + * @param viewHolder The ViewHolder which should be animated + * @param preLayoutInfo The information that was returned from + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * Might be null if Item was just added to the adapter or + * LayoutManager does not support predictive animations or it could + * not predict that this ViewHolder will become visible. + * @param postLayoutInfo The information that was returned from {@link + * #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. */ - void scrapView(View view) { - ViewHolder holder = getChildViewHolderInt(view); - if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) - || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { - if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) { - throw new IllegalArgumentException("Called scrap view with an invalid view." - + " Invalid views cannot be reused from scrap, they should rebound from" - + " recycler pool." + exceptionLabel()); - } - holder.setScrapContainer(this, false); - mAttachedScrap.add(holder); - } else { - if (mChangedScrap == null) { - mChangedScrap = new ArrayList<>(); - } - holder.setScrapContainer(this, true); - mChangedScrap.add(holder); - } - } + public abstract boolean animateAppearance(@NonNull ViewHolder viewHolder, + @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo); /** - * Remove a previously scrapped view from the pool of eligible scrap. + * Called by the RecyclerView when a ViewHolder is present in both before and after the + * layout and RecyclerView has not received a {@link Adapter#notifyItemChanged(int)} call + * for it or a {@link Adapter#notifyDataSetChanged()} call. + *

    + * This ViewHolder still represents the same data that it was representing when the layout + * started but its position / size may be changed by the LayoutManager. + *

    + * If the Item's layout position didn't change, RecyclerView still calls this method because + * it does not track this information (or does not necessarily know that an animation is + * not required). Your ItemAnimator should handle this case and if there is nothing to + * animate, it should call {@link #dispatchAnimationFinished(ViewHolder)} and return + * false. + *

    + * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation + * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it + * decides not to animate the view). * - *

    This view will no longer be eligible for reuse until re-scrapped or - * until it is explicitly removed and recycled.

    + * @param viewHolder The ViewHolder which should be animated + * @param preLayoutInfo The information that was returned from + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @param postLayoutInfo The information that was returned from {@link + * #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. */ - void unscrapView(ViewHolder holder) { - if (holder.mInChangeScrap) { - mChangedScrap.remove(holder); - } else { - mAttachedScrap.remove(holder); - } - holder.mScrapContainer = null; - holder.mInChangeScrap = false; - holder.clearReturnedFromScrapFlag(); - } - - int getScrapCount() { - return mAttachedScrap.size(); - } - - View getScrapViewAt(int index) { - return mAttachedScrap.get(index).itemView; - } - - void clearScrap() { - mAttachedScrap.clear(); - if (mChangedScrap != null) { - mChangedScrap.clear(); - } - } - - ViewHolder getChangedScrapViewForPosition(int position) { - // If pre-layout, check the changed scrap for an exact match. - int changedScrapSize; - if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) { - return null; - } - // find by position - for (int i = 0; i < changedScrapSize; i++) { - ViewHolder holder = mChangedScrap.get(i); - if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) { - holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); - return holder; - } - } - // find by id - if (mAdapter.hasStableIds()) { - int offsetPosition = mAdapterHelper.findPositionOffset(position); - if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) { - long id = mAdapter.getItemId(offsetPosition); - for (int i = 0; i < changedScrapSize; i++) { - ViewHolder holder = mChangedScrap.get(i); - if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) { - holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); - return holder; - } - } - } - } - return null; - } + public abstract boolean animatePersistence(@NonNull ViewHolder viewHolder, + @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo); /** - * Returns a view for the position either from attach scrap, hidden children, or cache. + * Called by the RecyclerView when an adapter item is present both before and after the + * layout and RecyclerView has received a {@link Adapter#notifyItemChanged(int)} call + * for it. This method may also be called when + * {@link Adapter#notifyDataSetChanged()} is called and adapter has stable ids so that + * RecyclerView could still rebind views to the same ViewHolders. If viewType changes when + * {@link Adapter#notifyDataSetChanged()} is called, this method will not be called, + * instead, {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)} will be + * called for the new ViewHolder and the old one will be recycled. + *

    + * If this method is called due to a {@link Adapter#notifyDataSetChanged()} call, there is + * a good possibility that item contents didn't really change but it is rebound from the + * adapter. {@link DefaultItemAnimator} will skip animating the View if its location on the + * screen didn't change and your animator should handle this case as well and avoid creating + * unnecessary animations. + *

    + * When an item is updated, ItemAnimator has a chance to ask RecyclerView to keep the + * previous presentation of the item as-is and supply a new ViewHolder for the updated + * presentation (see: {@link #canReuseUpdatedViewHolder(ViewHolder, List)}. + * This is useful if you don't know the contents of the Item and would like + * to cross-fade the old and the new one ({@link DefaultItemAnimator} uses this technique). + *

    + * When you are writing a custom item animator for your layout, it might be more performant + * and elegant to re-use the same ViewHolder and animate the content changes manually. + *

    + * When {@link Adapter#notifyItemChanged(int)} is called, the Item's view type may change. + * If the Item's view type has changed or ItemAnimator returned false for + * this ViewHolder when {@link #canReuseUpdatedViewHolder(ViewHolder, List)} was called, the + * oldHolder and newHolder will be different ViewHolder instances + * which represent the same Item. In that case, only the new ViewHolder is visible + * to the LayoutManager but RecyclerView keeps old ViewHolder attached for animations. + *

    + * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} for each distinct + * ViewHolder when their animation is complete + * (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it decides not to + * animate the view). + *

    + * If oldHolder and newHolder are the same instance, you should call + * {@link #dispatchAnimationFinished(ViewHolder)} only once. + *

    + * Note that when a ViewHolder both changes and disappears in the same layout pass, the + * animation callback method which will be called by the RecyclerView depends on the + * ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the + * LayoutManager's decision whether to layout the changed version of a disappearing + * ViewHolder or not. RecyclerView will call + * {@code animateChange} instead of + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance} if and only if the ItemAnimator returns {@code false} from + * {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the + * LayoutManager lays out a new disappearing view that holds the updated information. + * Built-in LayoutManagers try to avoid laying out updated versions of disappearing views. * - * @param position Item position - * @param dryRun Does a dry run, finds the ViewHolder but does not remove - * @return a ViewHolder that can be re-used for this position. + * @param oldHolder The ViewHolder before the layout is started, might be the same + * instance with newHolder. + * @param newHolder The ViewHolder after the layout is finished, might be the same + * instance with oldHolder. + * @param preLayoutInfo The information that was returned from + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @param postLayoutInfo The information that was returned from {@link + * #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. */ - ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { - int scrapCount = mAttachedScrap.size(); - - // Try first for an exact, non-invalid match from scrap. - for (int i = 0; i < scrapCount; i++) { - ViewHolder holder = mAttachedScrap.get(i); - if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position - && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) { - holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); - return holder; - } - } - - if (!dryRun) { - View view = mChildHelper.findHiddenNonRemovedView(position); - if (view != null) { - // This View is good to be used. We just need to unhide, detach and move to the - // scrap list. - ViewHolder vh = getChildViewHolderInt(view); - mChildHelper.unhide(view); - int layoutIndex = mChildHelper.indexOfChild(view); - if (layoutIndex == NO_POSITION) { - throw new IllegalStateException("layout index should not be -1 after " - + "unhiding a view:" + vh + exceptionLabel()); - } - mChildHelper.detachViewFromParent(layoutIndex); - scrapView(view); - vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP - | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); - return vh; - } - } + public abstract boolean animateChange(@NonNull ViewHolder oldHolder, + @NonNull ViewHolder newHolder, + @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo); - // Search in our first-level recycled view cache. - int cacheSize = mCachedViews.size(); - for (int i = 0; i < cacheSize; i++) { - ViewHolder holder = mCachedViews.get(i); - // invalid view holders may be in cache if adapter has stable ids as they can be - // retrieved via getScrapOrCachedViewForId - if (!holder.isInvalid() && holder.getLayoutPosition() == position - && !holder.isAttachedToTransitionOverlay()) { - if (!dryRun) { - mCachedViews.remove(i); - } - if (DEBUG) { - Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position - + ") found match in cache: " + holder); - } - return holder; - } + @AdapterChanges + static int buildAdapterChangeFlagsForAnimations(ViewHolder viewHolder) { + int flags = viewHolder.mFlags & (FLAG_INVALIDATED | FLAG_REMOVED | FLAG_CHANGED); + if (viewHolder.isInvalid()) { + return FLAG_INVALIDATED; } - return null; - } - - ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) { - // Look in our attached views first - int count = mAttachedScrap.size(); - for (int i = count - 1; i >= 0; i--) { - ViewHolder holder = mAttachedScrap.get(i); - if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) { - if (type == holder.getItemViewType()) { - holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); - if (holder.isRemoved()) { - // this might be valid in two cases: - // > item is removed but we are in pre-layout pass - // >> do nothing. return as is. make sure we don't rebind - // > item is removed then added to another position and we are in - // post layout. - // >> remove removed and invalid flags, add update flag to rebind - // because item was invisible to us and we don't know what happened in - // between. - if (!mState.isPreLayout()) { - holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE - | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED); - } - } - return holder; - } else if (!dryRun) { - // if we are running animations, it is actually better to keep it in scrap - // but this would force layout manager to lay it out which would be bad. - // Recycle this scrap. Type mismatch. - mAttachedScrap.remove(i); - removeDetachedView(holder.itemView, false); - quickRecycleScrapView(holder.itemView); - } + if ((flags & FLAG_INVALIDATED) == 0) { + final int oldPos = viewHolder.getOldPosition(); + final int pos = viewHolder.getAbsoluteAdapterPosition(); + if (oldPos != NO_POSITION && pos != NO_POSITION && oldPos != pos) { + flags |= FLAG_MOVED; } } + return flags; + } - // Search the first-level cache - int cacheSize = mCachedViews.size(); - for (int i = cacheSize - 1; i >= 0; i--) { - ViewHolder holder = mCachedViews.get(i); - if (holder.getItemId() == id && !holder.isAttachedToTransitionOverlay()) { - if (type == holder.getItemViewType()) { - if (!dryRun) { - mCachedViews.remove(i); - } - return holder; - } else if (!dryRun) { - recycleCachedViewAt(i); - return null; - } - } + /** + * Called when there are pending animations waiting to be started. This state + * is governed by the return values from + * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateAppearance()}, + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange()} + * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animatePersistence()}, and + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance()}, which inform the RecyclerView that the ItemAnimator wants to be + * called later to start the associated animations. runPendingAnimations() will be scheduled + * to be run on the next frame. + */ + public abstract void runPendingAnimations(); + + /** + * Method called when an animation on a view should be ended immediately. + * This could happen when other events, like scrolling, occur, so that + * animating views can be quickly put into their proper end locations. + * Implementations should ensure that any animations running on the item + * are canceled and affected properties are set to their end values. + * Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished + * animation since the animations are effectively done when this method is called. + * + * @param item The item for which an animation should be stopped. + */ + public abstract void endAnimation(@NonNull ViewHolder item); + + /** + * Method called when all item animations should be ended immediately. + * This could happen when other events, like scrolling, occur, so that + * animating views can be quickly put into their proper end locations. + * Implementations should ensure that any animations running on any items + * are canceled and affected properties are set to their end values. + * Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished + * animation since the animations are effectively done when this method is called. + */ + public abstract void endAnimations(); + + /** + * Method which returns whether there are any item animations currently running. + * This method can be used to determine whether to delay other actions until + * animations end. + * + * @return true if there are any item animations currently running, false otherwise. + */ + public abstract boolean isRunning(); + + /** + * Method to be called by subclasses when an animation is finished. + *

    + * For each call RecyclerView makes to + * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateAppearance()}, + * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animatePersistence()}, or + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance()}, there + * should + * be a matching {@link #dispatchAnimationFinished(ViewHolder)} call by the subclass. + *

    + * For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange()}, subclass should call this method for both the oldHolder + * and newHolder (if they are not the same instance). + * + * @param viewHolder The ViewHolder whose animation is finished. + * @see #onAnimationFinished(ViewHolder) + */ + public final void dispatchAnimationFinished(@NonNull ViewHolder viewHolder) { + onAnimationFinished(viewHolder); + if (mListener != null) { + mListener.onAnimationFinished(viewHolder); } - return null; } - @SuppressWarnings("unchecked") - void dispatchViewRecycled(@NonNull ViewHolder holder) { - // TODO: Remove this once setRecyclerListener (currently deprecated) is deleted. - if (mRecyclerListener != null) { - mRecyclerListener.onViewRecycled(holder); - } + /** + * Called after {@link #dispatchAnimationFinished(ViewHolder)} is called by the + * ItemAnimator. + * + * @param viewHolder The ViewHolder whose animation is finished. There might still be other + * animations running on this ViewHolder. + * @see #dispatchAnimationFinished(ViewHolder) + */ + public void onAnimationFinished(@NonNull ViewHolder viewHolder) { + } - int listenerCount = mRecyclerListeners.size(); - for (int i = 0; i < listenerCount; i++) { - mRecyclerListeners.get(i).onViewRecycled(holder); - } - if (mAdapter != null) { - mAdapter.onViewRecycled(holder); - } - if (mState != null) { - mViewInfoStore.removeViewHolder(holder); - } - if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder); + /** + * Method to be called by subclasses when an animation is started. + *

    + * For each call RecyclerView makes to + * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateAppearance()}, + * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animatePersistence()}, or + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance()}, there should be a matching + * {@link #dispatchAnimationStarted(ViewHolder)} call by the subclass. + *

    + * For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange()}, subclass should call this method for both the oldHolder + * and newHolder (if they are not the same instance). + *

    + * If your ItemAnimator decides not to animate a ViewHolder, it should call + * {@link #dispatchAnimationFinished(ViewHolder)} without calling + * {@link #dispatchAnimationStarted(ViewHolder)}. + * + * @param viewHolder The ViewHolder whose animation is starting. + * @see #onAnimationStarted(ViewHolder) + */ + public final void dispatchAnimationStarted(@NonNull ViewHolder viewHolder) { + onAnimationStarted(viewHolder); } - void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter, - boolean compatibleWithPrevious) { - clear(); - poolingContainerDetach(oldAdapter, true); - getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, - compatibleWithPrevious); - maybeSendPoolingContainerAttach(); + /** + * Called when a new animation is started on the given ViewHolder. + * + * @param viewHolder The ViewHolder which started animating. Note that the ViewHolder + * might already be animating and this might be another animation. + * @see #dispatchAnimationStarted(ViewHolder) + */ + public void onAnimationStarted(@NonNull ViewHolder viewHolder) { + } - void offsetPositionRecordsForMove(int from, int to) { - int start, end, inBetweenOffset; - if (from < to) { - start = from; - end = to; - inBetweenOffset = -1; - } else { - start = to; - end = from; - inBetweenOffset = 1; - } - int cachedCount = mCachedViews.size(); - for (int i = 0; i < cachedCount; i++) { - ViewHolder holder = mCachedViews.get(i); - if (holder == null || holder.mPosition < start || holder.mPosition > end) { - continue; - } - if (holder.mPosition == from) { - holder.offsetPosition(to - from, false); + /** + * Like {@link #isRunning()}, this method returns whether there are any item + * animations currently running. Additionally, the listener passed in will be called + * when there are no item animations running, either immediately (before the method + * returns) if no animations are currently running, or when the currently running + * animations are {@link #dispatchAnimationsFinished() finished}. + * + *

    Note that the listener is transient - it is either called immediately and not + * stored at all, or stored only until it is called when running animations + * are finished sometime later.

    + * + * @param listener A listener to be called immediately if no animations are running + * or later when currently-running animations have finished. A null + * listener is + * equivalent to calling {@link #isRunning()}. + * @return true if there are any item animations currently running, false otherwise. + */ + public final boolean isRunning(@Nullable ItemAnimatorFinishedListener listener) { + boolean running = isRunning(); + if (listener != null) { + if (!running) { + listener.onAnimationsFinished(); } else { - holder.offsetPosition(inBetweenOffset, false); - } - if (DEBUG) { - Log.d(TAG, "offsetPositionRecordsForMove cached child " + i + " holder " - + holder); + mFinishedListeners.add(listener); } } + return running; } - void offsetPositionRecordsForInsert(int insertedAt, int count) { - int cachedCount = mCachedViews.size(); - for (int i = 0; i < cachedCount; i++) { - ViewHolder holder = mCachedViews.get(i); - if (holder != null && holder.mPosition >= insertedAt) { - if (DEBUG) { - Log.d(TAG, "offsetPositionRecordsForInsert cached " + i + " holder " - + holder + " now at position " + (holder.mPosition + count)); - } - // insertions only affect post layout hence don't apply them to pre-layout. - holder.offsetPosition(count, false); - } - } + /** + * When an item is changed, ItemAnimator can decide whether it wants to re-use + * the same ViewHolder for animations or RecyclerView should create a copy of the + * item and ItemAnimator will use both to run the animation (e.g. cross-fade). + *

    + * Note that this method will only be called if the {@link ViewHolder} still has the same + * type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive + * both {@link ViewHolder}s in the + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method. + *

    + * If your application is using change payloads, you can override + * {@link #canReuseUpdatedViewHolder(ViewHolder, List)} to decide based on payloads. + * + * @param viewHolder The ViewHolder which represents the changed item's old content. + * @return True if RecyclerView should just rebind to the same ViewHolder or false if + * RecyclerView should create a new ViewHolder and pass this ViewHolder to the + * ItemAnimator to animate. Default implementation returns true. + * @see #canReuseUpdatedViewHolder(ViewHolder, List) + */ + public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder) { + return true; + } + + /** + * When an item is changed, ItemAnimator can decide whether it wants to re-use + * the same ViewHolder for animations or RecyclerView should create a copy of the + * item and ItemAnimator will use both to run the animation (e.g. cross-fade). + *

    + * Note that this method will only be called if the {@link ViewHolder} still has the same + * type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive + * both {@link ViewHolder}s in the + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method. + * + * @param viewHolder The ViewHolder which represents the changed item's old content. + * @param payloads A non-null list of merged payloads that were sent with change + * notifications. Can be empty if the adapter is invalidated via + * {@link RecyclerView.Adapter#notifyDataSetChanged()}. The same list of + * payloads will be passed into + * {@link RecyclerView.Adapter#onBindViewHolder(ViewHolder, int, List)} + * method if this method returns true. + * @return True if RecyclerView should just rebind to the same ViewHolder or false if + * RecyclerView should create a new ViewHolder and pass this ViewHolder to the + * ItemAnimator to animate. Default implementation calls + * {@link #canReuseUpdatedViewHolder(ViewHolder)}. + * @see #canReuseUpdatedViewHolder(ViewHolder) + */ + public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder, + @NonNull List payloads) { + return canReuseUpdatedViewHolder(viewHolder); } /** - * @param removedFrom Remove start index - * @param count Remove count - * @param applyToPreLayout If true, changes will affect ViewHolder's pre-layout position, if - * false, they'll be applied before the second layout pass + * This method should be called by ItemAnimator implementations to notify + * any listeners that all pending and active item animations are finished. */ - void offsetPositionRecordsForRemove(int removedFrom, int count, boolean applyToPreLayout) { - int removedEnd = removedFrom + count; - int cachedCount = mCachedViews.size(); - for (int i = cachedCount - 1; i >= 0; i--) { - ViewHolder holder = mCachedViews.get(i); - if (holder != null) { - if (holder.mPosition >= removedEnd) { - if (DEBUG) { - Log.d(TAG, "offsetPositionRecordsForRemove cached " + i - + " holder " + holder + " now at position " - + (holder.mPosition - count)); - } - holder.offsetPosition(-count, applyToPreLayout); - } else if (holder.mPosition >= removedFrom) { - // Item for this view was removed. Dump it from the cache. - holder.addFlags(ViewHolder.FLAG_REMOVED); - recycleCachedViewAt(i); - } - } + public final void dispatchAnimationsFinished() { + final int count = mFinishedListeners.size(); + for (int i = 0; i < count; ++i) { + mFinishedListeners.get(i).onAnimationsFinished(); } + mFinishedListeners.clear(); } - void setViewCacheExtension(ViewCacheExtension extension) { - mViewCacheExtension = extension; - } - - private void maybeSendPoolingContainerAttach() { - if (mRecyclerPool != null - && mAdapter != null - && isAttachedToWindow()) { - mRecyclerPool.attachForPoolingContainer(mAdapter); - } + /** + * Returns a new {@link ItemHolderInfo} which will be used to store information about the + * ViewHolder. This information will later be passed into animate** methods. + *

    + * You can override this method if you want to extend {@link ItemHolderInfo} and provide + * your own instances. + * + * @return A new {@link ItemHolderInfo}. + */ + @NonNull + public ItemHolderInfo obtainHolderInfo() { + return new ItemHolderInfo(); } - private void poolingContainerDetach(Adapter adapter) { - poolingContainerDetach(adapter, false); + /** + * The interface to be implemented by listeners to animation events from this + * ItemAnimator. This is used internally and is not intended for developers to + * create directly. + */ + interface ItemAnimatorListener { + void onAnimationFinished(@NonNull ViewHolder item); } - private void poolingContainerDetach(Adapter adapter, boolean isBeingReplaced) { - if (mRecyclerPool != null) { - mRecyclerPool.detachForPoolingContainer(adapter, isBeingReplaced); - } + /** + * This interface is used to inform listeners when all pending or running animations + * in an ItemAnimator are finished. This can be used, for example, to delay an action + * in a data set until currently-running animations are complete. + * + * @see #isRunning(ItemAnimatorFinishedListener) + */ + public interface ItemAnimatorFinishedListener { + /** + * Notifies when all pending or running animations in an ItemAnimator are finished. + */ + void onAnimationsFinished(); } - void onAttachedToWindow() { - maybeSendPoolingContainerAttach(); - } + /** + * A simple data structure that holds information about an item's bounds. + * This information is used in calculating item animations. Default implementation of + * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)} and + * {@link #recordPostLayoutInformation(RecyclerView.State, ViewHolder)} returns this data + * structure. You can extend this class if you would like to keep more information about + * the Views. + *

    + * If you want to provide your own implementation but still use `super` methods to record + * basic information, you can override {@link #obtainHolderInfo()} to provide your own + * instances. + */ + public static class ItemHolderInfo { - void onDetachedFromWindow() { - for (int i = 0; i < mCachedViews.size(); i++) { - PoolingContainer.callPoolingContainerOnRelease(mCachedViews.get(i).itemView); - } - poolingContainerDetach(mAdapter); - } + /** + * The left edge of the View (excluding decorations) + */ + public int left; - RecycledViewPool getRecycledViewPool() { - if (mRecyclerPool == null) { - mRecyclerPool = new RecycledViewPool(); - maybeSendPoolingContainerAttach(); - } - return mRecyclerPool; - } + /** + * The top edge of the View (excluding decorations) + */ + public int top; - void setRecycledViewPool(RecycledViewPool pool) { - poolingContainerDetach(mAdapter); - if (mRecyclerPool != null) { - mRecyclerPool.detach(); - } - mRecyclerPool = pool; - if (mRecyclerPool != null && getAdapter() != null) { - mRecyclerPool.attach(); - } - maybeSendPoolingContainerAttach(); - } + /** + * The right edge of the View (excluding decorations) + */ + public int right; - void viewRangeUpdate(int positionStart, int itemCount) { - int positionEnd = positionStart + itemCount; - int cachedCount = mCachedViews.size(); - for (int i = cachedCount - 1; i >= 0; i--) { - ViewHolder holder = mCachedViews.get(i); - if (holder == null) { - continue; - } + /** + * The bottom edge of the View (excluding decorations) + */ + public int bottom; - int pos = holder.mPosition; - if (pos >= positionStart && pos < positionEnd) { - holder.addFlags(ViewHolder.FLAG_UPDATE); - recycleCachedViewAt(i); - // cached views should not be flagged as changed because this will cause them - // to animate when they are returned from cache. - } - } - } + /** + * The change flags that were passed to + * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)}. + */ + @AdapterChanges + public int changeFlags; - void markKnownViewsInvalid() { - int cachedCount = mCachedViews.size(); - for (int i = 0; i < cachedCount; i++) { - ViewHolder holder = mCachedViews.get(i); - if (holder != null) { - holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID); - holder.addChangePayload(null); - } + public ItemHolderInfo() { } - if (mAdapter == null || !mAdapter.hasStableIds()) { - // we cannot re-use cached views in this case. Recycle them all - recycleAndClearCachedViews(); + /** + * Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from + * the given ViewHolder. Clears all {@link #changeFlags}. + * + * @param holder The ViewHolder whose bounds should be copied. + * @return This {@link ItemHolderInfo} + */ + @NonNull + public ItemHolderInfo setFrom(@NonNull RecyclerView.ViewHolder holder) { + return setFrom(holder, 0); } - } - void clearOldPositions() { - int cachedCount = mCachedViews.size(); - for (int i = 0; i < cachedCount; i++) { - ViewHolder holder = mCachedViews.get(i); - holder.clearOldPosition(); - } - int scrapCount = mAttachedScrap.size(); - for (int i = 0; i < scrapCount; i++) { - mAttachedScrap.get(i).clearOldPosition(); - } - if (mChangedScrap != null) { - int changedScrapCount = mChangedScrap.size(); - for (int i = 0; i < changedScrapCount; i++) { - mChangedScrap.get(i).clearOldPosition(); - } + /** + * Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from + * the given ViewHolder and sets the {@link #changeFlags} to the given flags parameter. + * + * @param holder The ViewHolder whose bounds should be copied. + * @param flags The adapter change flags that were passed into + * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, + * List)}. + * @return This {@link ItemHolderInfo} + */ + @NonNull + public ItemHolderInfo setFrom(@NonNull RecyclerView.ViewHolder holder, + @AdapterChanges int flags) { + final View view = holder.itemView; + this.left = view.getLeft(); + this.top = view.getTop(); + this.right = view.getRight(); + this.bottom = view.getBottom(); + return this; } } + } - void markItemDecorInsetsDirty() { - int cachedCount = mCachedViews.size(); - for (int i = 0; i < cachedCount; i++) { - ViewHolder holder = mCachedViews.get(i); - LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams(); - if (layoutParams != null) { - layoutParams.mInsetsDirty = true; - } - } + @Override + protected int getChildDrawingOrder(int childCount, int i) { + if (mChildDrawingOrderCallback == null) { + return super.getChildDrawingOrder(childCount, i); + } else { + return mChildDrawingOrderCallback.onGetChildDrawingOrder(childCount, i); } } /** - * Internal listener that manages items after animations finish. This is how items are - * retained (not recycled) during animations, but allowed to be recycled afterwards. - * It depends on the contract with the ItemAnimator to call the appropriate dispatch*Finished() - * method on the animator's listener when it is done animating any item. + * A callback interface that can be used to alter the drawing order of RecyclerView children. + *

    + * It works using the {@link ViewGroup#getChildDrawingOrder(int, int)} method, so any case + * that applies to that method also applies to this callback. For example, changing the drawing + * order of two views will not have any effect if their elevation values are different since + * elevation overrides the result of this callback. */ - private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener { - - ItemAnimatorRestoreListener() { - } + public interface ChildDrawingOrderCallback { + /** + * Returns the index of the child to draw for this iteration. Override this + * if you want to change the drawing order of children. By default, it + * returns i. + * + * @param i The current iteration. + * @return The index of the child to draw this iteration. + * @see RecyclerView#setChildDrawingOrderCallback(RecyclerView.ChildDrawingOrderCallback) + */ + int onGetChildDrawingOrder(int childCount, int i); + } - @Override - public void onAnimationFinished(ViewHolder item) { - item.setIsRecyclable(true); - if (item.mShadowedHolder != null && item.mShadowingHolder == null) { // old vh - item.mShadowedHolder = null; - } - // always null this because an OldViewHolder can never become NewViewHolder w/o being - // recycled. - item.mShadowingHolder = null; - if (!item.shouldBeKeptAsChild()) { - if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) { - removeDetachedView(item.itemView, false); - } - } + private NestedScrollingChildHelper getScrollingChildHelper() { + if (mScrollingChildHelper == null) { + mScrollingChildHelper = new NestedScrollingChildHelper(this); } + return mScrollingChildHelper; } } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/RecyclerViewAccessibilityDelegate.java b/viewpager2/src/main/java/androidx/recyclerview/widget/RecyclerViewAccessibilityDelegate.java index daf28e37c..fd9d86c34 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/RecyclerViewAccessibilityDelegate.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/RecyclerViewAccessibilityDelegate.java @@ -119,7 +119,7 @@ public AccessibilityDelegateCompat getItemDelegate() { */ public static class ItemDelegate extends AccessibilityDelegateCompat { final RecyclerViewAccessibilityDelegate mRecyclerViewDelegate; - private final Map mOriginalItemDelegates = new WeakHashMap<>(); + private Map mOriginalItemDelegates = new WeakHashMap<>(); /** * Creates an item delegate for the given {@code RecyclerViewAccessibilityDelegate}. @@ -152,7 +152,7 @@ AccessibilityDelegateCompat getAndRemoveOriginalDelegateForItem(View itemView) { public void onInitializeAccessibilityNodeInfo( @SuppressLint("InvalidNullabilityOverride") @NonNull View host, @SuppressLint("InvalidNullabilityOverride") @NonNull - AccessibilityNodeInfoCompat info + AccessibilityNodeInfoCompat info ) { if (!mRecyclerViewDelegate.shouldIgnore() && mRecyclerViewDelegate.mRecyclerView.getLayoutManager() != null) { @@ -204,7 +204,7 @@ public void sendAccessibilityEvent(@NonNull View host, int eventType) { @Override public void sendAccessibilityEventUnchecked(@NonNull View host, - @NonNull AccessibilityEvent event) { + @NonNull AccessibilityEvent event) { AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); if (originalDelegate != null) { originalDelegate.sendAccessibilityEventUnchecked(host, event); @@ -215,7 +215,7 @@ public void sendAccessibilityEventUnchecked(@NonNull View host, @Override public boolean dispatchPopulateAccessibilityEvent(@NonNull View host, - @NonNull AccessibilityEvent event) { + @NonNull AccessibilityEvent event) { AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); if (originalDelegate != null) { return originalDelegate.dispatchPopulateAccessibilityEvent(host, event); @@ -226,7 +226,7 @@ public boolean dispatchPopulateAccessibilityEvent(@NonNull View host, @Override public void onPopulateAccessibilityEvent(@NonNull View host, - @NonNull AccessibilityEvent event) { + @NonNull AccessibilityEvent event) { AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); if (originalDelegate != null) { originalDelegate.onPopulateAccessibilityEvent(host, event); @@ -237,7 +237,7 @@ public void onPopulateAccessibilityEvent(@NonNull View host, @Override public void onInitializeAccessibilityEvent(@NonNull View host, - @NonNull AccessibilityEvent event) { + @NonNull AccessibilityEvent event) { AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); if (originalDelegate != null) { originalDelegate.onInitializeAccessibilityEvent(host, event); @@ -248,7 +248,7 @@ public void onInitializeAccessibilityEvent(@NonNull View host, @Override public boolean onRequestSendAccessibilityEvent(@NonNull ViewGroup host, - @NonNull View child, @NonNull AccessibilityEvent event) { + @NonNull View child, @NonNull AccessibilityEvent event) { AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); if (originalDelegate != null) { return originalDelegate.onRequestSendAccessibilityEvent(host, child, event); diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/ScrollbarHelper.java b/viewpager2/src/main/java/androidx/recyclerview/widget/ScrollbarHelper.java index aaf5d03bc..8cc24aeb1 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/ScrollbarHelper.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/ScrollbarHelper.java @@ -22,35 +22,32 @@ */ class ScrollbarHelper { - private ScrollbarHelper() { - } - /** * @param startChild View closest to start of the list. (top or left) * @param endChild View closest to end of the list (bottom or right) */ static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation, - View startChild, View endChild, RecyclerView.LayoutManager lm, - boolean smoothScrollbarEnabled, boolean reverseLayout) { + View startChild, View endChild, RecyclerView.LayoutManager lm, + boolean smoothScrollbarEnabled, boolean reverseLayout) { if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null || endChild == null) { return 0; } - int minPosition = Math.min(lm.getPosition(startChild), + final int minPosition = Math.min(lm.getPosition(startChild), lm.getPosition(endChild)); - int maxPosition = Math.max(lm.getPosition(startChild), + final int maxPosition = Math.max(lm.getPosition(startChild), lm.getPosition(endChild)); - int itemsBefore = reverseLayout + final int itemsBefore = reverseLayout ? Math.max(0, state.getItemCount() - maxPosition - 1) : Math.max(0, minPosition); if (!smoothScrollbarEnabled) { return itemsBefore; } - int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild) + final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild) - orientation.getDecoratedStart(startChild)); - int itemRange = Math.abs(lm.getPosition(startChild) + final int itemRange = Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1; - float avgSizePerRow = (float) laidOutArea / itemRange; + final float avgSizePerRow = (float) laidOutArea / itemRange; return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding() - orientation.getDecoratedStart(startChild))); @@ -61,8 +58,8 @@ static int computeScrollOffset(RecyclerView.State state, OrientationHelper orien * @param endChild View closest to end of the list (bottom or right) */ static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation, - View startChild, View endChild, RecyclerView.LayoutManager lm, - boolean smoothScrollbarEnabled) { + View startChild, View endChild, RecyclerView.LayoutManager lm, + boolean smoothScrollbarEnabled) { if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null || endChild == null) { return 0; @@ -70,7 +67,7 @@ static int computeScrollExtent(RecyclerView.State state, OrientationHelper orien if (!smoothScrollbarEnabled) { return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1; } - int extend = orientation.getDecoratedEnd(endChild) + final int extend = orientation.getDecoratedEnd(endChild) - orientation.getDecoratedStart(startChild); return Math.min(orientation.getTotalSpace(), extend); } @@ -80,8 +77,8 @@ static int computeScrollExtent(RecyclerView.State state, OrientationHelper orien * @param endChild View closest to end of the list (bottom or right) */ static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation, - View startChild, View endChild, RecyclerView.LayoutManager lm, - boolean smoothScrollbarEnabled) { + View startChild, View endChild, RecyclerView.LayoutManager lm, + boolean smoothScrollbarEnabled) { if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null || endChild == null) { return 0; @@ -90,12 +87,15 @@ static int computeScrollRange(RecyclerView.State state, OrientationHelper orient return state.getItemCount(); } // smooth scrollbar enabled. try to estimate better. - int laidOutArea = orientation.getDecoratedEnd(endChild) + final int laidOutArea = orientation.getDecoratedEnd(endChild) - orientation.getDecoratedStart(startChild); - int laidOutRange = Math.abs(lm.getPosition(startChild) + final int laidOutRange = Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1; // estimate a size for full list. return (int) ((float) laidOutArea / laidOutRange * state.getItemCount()); } + + private ScrollbarHelper() { + } } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/SimpleItemAnimator.java b/viewpager2/src/main/java/androidx/recyclerview/widget/SimpleItemAnimator.java index ca5f68e5a..4dc33c631 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/SimpleItemAnimator.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/SimpleItemAnimator.java @@ -77,6 +77,7 @@ public void setSupportsChangeAnimations(boolean supportsChangeAnimations) { * * @return True if change animations are not supported or the ViewHolder is invalid, * false otherwise. + * * @see #setSupportsChangeAnimations(boolean) */ @Override @@ -86,7 +87,7 @@ public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHo @Override public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, - @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { + @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { int oldLeft = preLayoutInfo.left; int oldTop = preLayoutInfo.top; View disappearingItemView = viewHolder.itemView; @@ -110,7 +111,7 @@ public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, @Override public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, - @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { + @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { if (preLayoutInfo != null && (preLayoutInfo.left != postLayoutInfo.left || preLayoutInfo.top != postLayoutInfo.top)) { // slide items in if before/after locations differ @@ -129,7 +130,7 @@ public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, @Override public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder, - @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { + @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { if (preLayoutInfo.left != postLayoutInfo.left || preLayoutInfo.top != postLayoutInfo.top) { if (DEBUG) { Log.d(TAG, "PERSISTENT: " + viewHolder @@ -144,14 +145,14 @@ public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder, @Override public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, - @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preLayoutInfo, - @NonNull ItemHolderInfo postLayoutInfo) { + @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preLayoutInfo, + @NonNull ItemHolderInfo postLayoutInfo) { if (DEBUG) { Log.d(TAG, "CHANGED: " + oldHolder + " with view " + oldHolder.itemView); } - int fromLeft = preLayoutInfo.left; - int fromTop = preLayoutInfo.top; - int toLeft, toTop; + final int fromLeft = preLayoutInfo.left; + final int fromTop = preLayoutInfo.top; + final int toLeft, toTop; if (newHolder.shouldIgnore()) { toLeft = preLayoutInfo.left; toTop = preLayoutInfo.top; @@ -234,7 +235,7 @@ public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, */ @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public abstract boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, - int toX, int toY); + int toX, int toY); /** * Called when an item is changed in the RecyclerView, as indicated by a call to @@ -270,7 +271,7 @@ public abstract boolean animateMove(RecyclerView.ViewHolder holder, int fromX, i */ @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public abstract boolean animateChange(RecyclerView.ViewHolder oldHolder, - RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop); + RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop); /** * Method to be called by subclasses when a remove animation is done. @@ -474,4 +475,5 @@ public void onChangeStarting(RecyclerView.ViewHolder item, boolean oldItem) { @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public void onChangeFinished(RecyclerView.ViewHolder item, boolean oldItem) { } -} \ No newline at end of file +} + diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/SnapHelper.java b/viewpager2/src/main/java/androidx/recyclerview/widget/SnapHelper.java index 45b9c6751..5ba267b38 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/SnapHelper.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/SnapHelper.java @@ -37,10 +37,12 @@ public abstract class SnapHelper extends RecyclerView.OnFlingListener { static final float MILLISECONDS_PER_INCH = 100f; RecyclerView mRecyclerView; + private Scroller mGravityScroller; + // Handles the snap on scroll case. private final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() { - boolean mScrolled; + boolean mScrolled = false; @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { @@ -58,7 +60,6 @@ public void onScrolled(RecyclerView recyclerView, int dx, int dy) { } } }; - private Scroller mGravityScroller; @Override public boolean onFling(int velocityX, int velocityY) { @@ -83,8 +84,10 @@ public boolean onFling(int velocityX, int velocityY) { * @param recyclerView The RecyclerView instance to which you want to add this helper or * {@code null} if you want to remove SnapHelper from the current * RecyclerView. + * * @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener} - * attached to the provided {@link RecyclerView}. + * attached to the provided {@link RecyclerView}. + * */ public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException { @@ -125,8 +128,9 @@ private void destroyCallbacks() { /** * Calculated the estimated scroll distance in each direction given velocities on both axes. * - * @param velocityX Fling velocity on the horizontal axis. - * @param velocityY Fling velocity on the vertical axis. + * @param velocityX Fling velocity on the horizontal axis. + * @param velocityY Fling velocity on the vertical axis. + * * @return array holding the calculated distances in x and y directions * respectively. */ @@ -147,10 +151,11 @@ public int[] calculateScrollDistance(int velocityX, int velocityY) { * {@link RecyclerView}. * @param velocityX Fling velocity on the horizontal axis. * @param velocityY Fling velocity on the vertical axis. + * * @return true if it is handled, false otherwise. */ private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX, - int velocityY) { + int velocityY) { if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { return false; } @@ -196,8 +201,9 @@ void snapToTargetExistingView() { /** * Creates a scroller to be used in the snapping implementation. * - * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached - * {@link RecyclerView}. + * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached + * {@link RecyclerView}. + * * @return a {@link RecyclerView.SmoothScroller} which will handle the scrolling. */ @Nullable @@ -209,8 +215,9 @@ protected RecyclerView.SmoothScroller createScroller( /** * Creates a scroller to be used in the snapping implementation. * - * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached - * {@link RecyclerView}. + * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached + * {@link RecyclerView}. + * * @return a {@link LinearSmoothScroller} which will handle the scrolling. * @deprecated use {@link #createScroller(RecyclerView.LayoutManager)} instead. */ @@ -230,9 +237,9 @@ protected void onTargetFound(View targetView, RecyclerView.State state, Action a } int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView); - int dx = snapDistances[0]; - int dy = snapDistances[1]; - int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))); + final int dx = snapDistances[0]; + final int dy = snapDistances[1]; + final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))); if (time > 0) { action.update(dx, dy, time, mDecelerateInterpolator); } @@ -254,14 +261,15 @@ protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { * * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached * {@link RecyclerView} - * @param targetView the target view that is chosen as the view to snap + * @param targetView the target view that is chosen as the view to snap + * * @return the output coordinates the put the result into. out[0] is the distance * on horizontal axis and out[1] is the distance on vertical axis. */ @SuppressWarnings("WeakerAccess") @Nullable public abstract int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, - @NonNull View targetView); + @NonNull View targetView); /** * Override this method to provide a particular target view for snapping. @@ -275,6 +283,7 @@ public abstract int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutM * * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached * {@link RecyclerView} + * * @return the target view to which to snap on fling or end of scroll */ @SuppressWarnings("WeakerAccess") @@ -287,12 +296,13 @@ public abstract int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutM * * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached * {@link RecyclerView} - * @param velocityX fling velocity on the horizontal axis - * @param velocityY fling velocity on the vertical axis + * @param velocityX fling velocity on the horizontal axis + * @param velocityY fling velocity on the vertical axis + * * @return the target adapter position to you want to snap or {@link RecyclerView#NO_POSITION} - * if no snapping should happen + * if no snapping should happen */ @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly public abstract int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, - int velocityX, int velocityY); -} \ No newline at end of file + int velocityX, int velocityY); +} diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/SortedList.java b/viewpager2/src/main/java/androidx/recyclerview/widget/SortedList.java index c83ce8132..3ae96b81f 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/SortedList.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/SortedList.java @@ -51,31 +51,37 @@ public class SortedList { private static final int INSERTION = 1; private static final int DELETION = 1 << 1; private static final int LOOKUP = 1 << 2; - private final Class mTClass; T[] mData; + /** * A reference to the previous set of data that is kept during a mutation operation (addAll or * replaceAll). */ private T[] mOldData; + /** * The current index into mOldData that has not yet been processed during a mutation operation * (addAll or replaceAll). */ private int mOldDataStart; private int mOldDataSize; + /** * The current index into the new data that has not yet been processed during a mutation * operation (addAll or replaceAll). */ private int mNewDataStart; + /** * The callback instance that controls the behavior of the SortedList and get notified when * changes happen. */ private Callback mCallback; + private BatchedCallback mBatchedCallback; + private int mSize; + private final Class mTClass; /** * Creates a new SortedList of type T. @@ -129,6 +135,7 @@ public int size() { * {@link #indexOf(Object)} before you update the object. * * @param item The item to be added into the list. + * * @return The index of the newly added item. * @see Callback#compare(Object, Object) * @see Callback#areItemsTheSame(Object, Object) @@ -148,8 +155,7 @@ public int add(T item) { * extra memory allocation, in which case you should not continue to reference or modify the * array yourself. *

    - * - * @param items Array of items to be added into the list. + * @param items Array of items to be added into the list. * @param mayModifyInput If true, SortedList is allowed to modify and permanently reference the * input array. * @see SortedList#addAll(T[] items) @@ -170,8 +176,9 @@ public void addAll(@NonNull T[] items, boolean mayModifyInput) { /** * Adds the given items to the list. Does not modify or retain the input. * - * @param items Array of items to be added into the list. * @see SortedList#addAll(T[] items, boolean mayModifyInput) + * + * @param items Array of items to be added into the list. */ public void addAll(@NonNull T... items) { addAll(items, false); @@ -180,8 +187,9 @@ public void addAll(@NonNull T... items) { /** * Adds the given items to the list. Does not modify or retain the input. * - * @param items Collection of items to be added into the list. * @see SortedList#addAll(T[] items, boolean mayModifyInput) + * + * @param items Collection of items to be added into the list. */ public void addAll(@NonNull Collection items) { T[] copy = (T[]) Array.newInstance(mTClass, items.size()); @@ -202,8 +210,7 @@ public void addAll(@NonNull Collection items) { * and {@link ListUpdateCallback#onRemoved(int, int)} events. See {@link DiffUtil} if you want * your implementation to dispatch move events. *

    - * - * @param items Array of items to replace current items. + * @param items Array of items to replace current items. * @param mayModifyInput If true, SortedList is allowed to modify and permanently reference the * input array. * @see #replaceAll(T[]) @@ -222,8 +229,9 @@ public void replaceAll(@NonNull T[] items, boolean mayModifyInput) { * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events * for each change detected as appropriate. Does not modify or retain the input. * - * @param items Array of items to replace current items. * @see #replaceAll(T[], boolean) + * + * @param items Array of items to replace current items. */ public void replaceAll(@NonNull T... items) { replaceAll(items, false); @@ -233,8 +241,9 @@ public void replaceAll(@NonNull T... items) { * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events * for each change detected as appropriate. Does not modify or retain the input. * - * @param items Array of items to replace current items. * @see #replaceAll(T[], boolean) + * + * @param items Array of items to replace current items. */ public void replaceAll(@NonNull Collection items) { T[] copy = (T[]) Array.newInstance(mTClass, items.size()); @@ -246,7 +255,7 @@ private void addAllInternal(T[] newItems) { return; } - int newSize = sortAndDedup(newItems); + final int newSize = sortAndDedup(newItems); if (mSize == 0) { mData = newItems; @@ -258,7 +267,7 @@ private void addAllInternal(T[] newItems) { } private void replaceAllInternal(@NonNull T[] newData) { - boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback); + final boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback); if (forceBatchedUpdates) { beginBatchedUpdates(); } @@ -362,7 +371,7 @@ private int sortAndDedup(@NonNull T[] items) { if (compare == 0) { // The range of equal items continues, update it. - int sameItemPos = findSameItem(currentItem, items, rangeStart, rangeEnd); + final int sameItemPos = findSameItem(currentItem, items, rangeStart, rangeEnd); if (sameItemPos != INVALID_POSITION) { // Replace the duplicate item. items[sameItemPos] = currentItem; @@ -398,7 +407,7 @@ private int findSameItem(T item, T[] items, int from, int to) { * This method assumes that newItems are sorted and deduplicated. */ private void merge(T[] newData, int newDataSize) { - boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback); + final boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback); if (forceBatchedUpdates) { beginBatchedUpdates(); } @@ -407,7 +416,7 @@ private void merge(T[] newData, int newDataSize) { mOldDataStart = 0; mOldDataSize = mSize; - int mergedCapacity = mSize + newDataSize + CAPACITY_GROWTH; + final int mergedCapacity = mSize + newDataSize + CAPACITY_GROWTH; mData = (T[]) Array.newInstance(mTClass, mergedCapacity); mNewDataStart = 0; @@ -560,6 +569,7 @@ private int add(T item, boolean notify) { * Removes the provided item from the list and calls {@link Callback#onRemoved(int, int)}. * * @param item The item to be removed from the list. + * * @return True if item is removed, false if item cannot be found in the list. */ public boolean remove(T item) { @@ -571,6 +581,7 @@ public boolean remove(T item) { * Removes the item at the given index and calls {@link Callback#onRemoved(int, int)}. * * @param index The index of the item to be removed. + * * @return The removed item. */ public T removeItemAt(int index) { @@ -620,12 +631,12 @@ private void removeItemAtIndex(int index, boolean notify) { */ public void updateItemAt(int index, T item) { throwIfInMutationOperation(); - T existing = get(index); + final T existing = get(index); // assume changed if the same object is given back boolean contentsChanged = existing == item || !mCallback.areContentsTheSame(existing, item); if (existing != item) { // different items, we can use comparison and may avoid lookup - int cmp = mCallback.compare(existing, item); + final int cmp = mCallback.compare(existing, item); if (cmp == 0) { mData[index] = item; if (contentsChanged) { @@ -675,7 +686,7 @@ public void updateItemAt(int index, T item) { public void recalculatePositionOfItemAt(int index) { throwIfInMutationOperation(); // TODO can be improved - T item = get(index); + final T item = get(index); removeItemAtIndex(index, false); int newIndex = add(item, false); if (index != newIndex) { @@ -687,6 +698,7 @@ public void recalculatePositionOfItemAt(int index) { * Returns the item at the given index. * * @param index The index of the item to retrieve. + * * @return The item at the given index. * @throws java.lang.IndexOutOfBoundsException if provided index is negative or larger than the * size of the list. @@ -710,6 +722,7 @@ public T get(int index) throws IndexOutOfBoundsException { * Returns the position of the provided item. * * @param item The item to query for position. + * * @return The position of the provided item or {@link #INVALID_POSITION} if item is not in the * list. */ @@ -730,9 +743,9 @@ public int indexOf(T item) { private int findIndexOf(T item, T[] mData, int left, int right, int reason) { while (left < right) { - int middle = (left + right) / 2; + final int middle = (left + right) / 2; T myItem = mData[middle]; - int cmp = mCallback.compare(myItem, item); + final int cmp = mCallback.compare(myItem, item); if (cmp < 0) { left = middle + 1; } else if (cmp == 0) { @@ -812,7 +825,7 @@ public void clear() { if (mSize == 0) { return; } - int prevSize = mSize; + final int prevSize = mSize; Arrays.fill(mData, 0, prevSize, null); mSize = 0; mCallback.onRemoved(0, prevSize); @@ -834,6 +847,7 @@ public static abstract class Callback implements Comparator, ListUpdateC * * @param o1 The first object to compare. * @param o2 The second object to compare. + * * @return a negative integer, zero, or a positive integer as the * first argument is less than, equal to, or greater than the * second. @@ -870,6 +884,7 @@ public void onChanged(int position, int count, Object payload) { * * @param oldItem The previous representation of the object. * @param newItem The new object that replaces the previous one. + * * @return True if the contents of the items are the same or false if they are different. */ abstract public boolean areContentsTheSame(T2 oldItem, T2 newItem); @@ -881,6 +896,7 @@ public void onChanged(int position, int count, Object payload) { * * @param item1 The first item to check. * @param item2 The second item to check. + * * @return True if the two items represent the same object or false if they are different. */ abstract public boolean areItemsTheSame(T2 item1, T2 item2); @@ -928,7 +944,6 @@ public static class BatchedCallback extends Callback { final Callback mWrappedCallback; private final BatchingListUpdateCallback mBatchingListUpdateCallback; - /** * Creates a new BatchedCallback that wraps the provided Callback. * @@ -997,4 +1012,4 @@ public void dispatchLastEvent() { mBatchingListUpdateCallback.dispatchLastEvent(); } } -} \ No newline at end of file +} diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/SortedListAdapterCallback.java b/viewpager2/src/main/java/androidx/recyclerview/widget/SortedListAdapterCallback.java index 46fc269aa..639a26a69 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/SortedListAdapterCallback.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/SortedListAdapterCallback.java @@ -64,4 +64,4 @@ public void onChanged(int position, int count) { public void onChanged(int position, int count, Object payload) { mAdapter.notifyItemRangeChanged(position, count, payload); } -} \ No newline at end of file +} diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/StableIdStorage.java b/viewpager2/src/main/java/androidx/recyclerview/widget/StableIdStorage.java index f3e6e4b10..d157e9f5e 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/StableIdStorage.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/StableIdStorage.java @@ -67,7 +67,7 @@ public StableIdLookup createStableIdLookup() { * and always replaces the local id w/ a globally available ID to be consistent. */ class IsolatedStableIdStorage implements StableIdStorage { - long mNextStableId; + long mNextStableId = 0; long obtainId() { return mNextStableId++; diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java b/viewpager2/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java index fa6041a53..b8b3e7a4f 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java @@ -32,10 +32,12 @@ import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; +import java.util.List; /** * A LayoutManager that lays out children in a staggered grid formation. @@ -48,18 +50,26 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider { + private static final String TAG = "StaggeredGridLManager"; + + static final boolean DEBUG = false; + public static final int HORIZONTAL = RecyclerView.HORIZONTAL; + public static final int VERTICAL = RecyclerView.VERTICAL; + /** * Does not do anything to hide gaps. */ public static final int GAP_HANDLING_NONE = 0; + /** * @deprecated No longer supported. */ @SuppressWarnings("unused") @Deprecated public static final int GAP_HANDLING_LAZY = 1; + /** * When scroll state is changed to {@link RecyclerView#SCROLL_STATE_IDLE}, StaggeredGrid will * check if there are gaps in the because of full span items. If it finds, it will re-layout @@ -80,36 +90,22 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple * */ public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2; - static final boolean DEBUG = false; + static final int INVALID_OFFSET = Integer.MIN_VALUE; - private static final String TAG = "StaggeredGridLManager"; /** * While trying to find next view to focus, LayoutManager will not try to scroll more * than this factor times the total space of the list. If layout is vertical, total space is the * height minus padding, if layout is horizontal, total space is the width minus padding. */ private static final float MAX_SCROLL_FACTOR = 1 / 3f; + /** - * Keeps the mapping between the adapter positions and spans. This is necessary to provide - * a consistent experience when user scrolls the list. - */ - final LazySpanLookup mLazySpanLookup = new LazySpanLookup(); - @NonNull - private final LayoutState mLayoutState; - /** - * Re-used rectangle to get child decor offsets. - */ - private final Rect mTmpRect = new Rect(); - /** - * Re-used anchor info. - */ - private final AnchorInfo mAnchorInfo = new AnchorInfo(); - /** - * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. - * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} + * Number of spans */ - private final boolean mSmoothScrollbarEnabled = true; + private int mSpanCount = -1; + Span[] mSpans; + /** * Primary orientation is the layout's orientation, secondary orientation is the orientation * for spans. Having both makes code much cleaner for calculations. @@ -118,76 +114,112 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple OrientationHelper mPrimaryOrientation; @NonNull OrientationHelper mSecondaryOrientation; - boolean mReverseLayout; + + private int mOrientation; + + /** + * The width or height per span, depending on the orientation. + */ + private int mSizePerSpan; + + @NonNull + private final LayoutState mLayoutState; + + boolean mReverseLayout = false; + /** * Aggregated reverse layout value that takes RTL into account. */ - boolean mShouldReverseLayout; + boolean mShouldReverseLayout = false; + + /** + * Temporary variable used during fill method to check which spans needs to be filled. + */ + private BitSet mRemainingSpans; + /** * When LayoutManager needs to scroll to a position, it sets this variable and requests a * layout which will check this variable and re-layout accordingly. */ int mPendingScrollPosition = RecyclerView.NO_POSITION; + /** * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is * called. */ int mPendingScrollPositionOffset = INVALID_OFFSET; + /** - * Number of spans - */ - private int mSpanCount = -1; - private int mOrientation; - /** - * The width or height per span, depending on the orientation. - */ - private int mSizePerSpan; - /** - * Temporary variable used during fill method to check which spans needs to be filled. + * Keeps the mapping between the adapter positions and spans. This is necessary to provide + * a consistent experience when user scrolls the list. */ - private BitSet mRemainingSpans; + LazySpanLookup mLazySpanLookup = new LazySpanLookup(); + /** * how we handle gaps in UI. */ private int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; + /** * Saved state needs this information to properly layout on restore. */ private boolean mLastLayoutFromEnd; + /** * Saved state and onLayout needs this information to re-layout properly */ private boolean mLastLayoutRTL; + /** * SavedState is not handled until a layout happens. This is where we keep it until next * layout. */ private StaggeredGridLayoutManager_SavedState mPendingSavedState; + /** * Re-used measurement specs. updated by onLayout. */ private int mFullSizeSpec; + + /** + * Re-used rectangle to get child decor offsets. + */ + private final Rect mTmpRect = new Rect(); + + /** + * Re-used anchor info. + */ + private final AnchorInfo mAnchorInfo = new AnchorInfo(); + /** * If a full span item is invalid / or created in reverse direction; it may create gaps in * the UI. While laying out, if such case is detected, we set this flag. *

    * After scrolling stops, we check this flag and if it is set, re-layout. */ - private boolean mLaidOutInvalidFullSpan; - private final Runnable mCheckForGapsRunnable = this::checkForGaps; + private boolean mLaidOutInvalidFullSpan = false; + + /** + * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. + * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} + */ + private boolean mSmoothScrollbarEnabled = true; + /** * Temporary array used (solely in {@link #collectAdjacentPrefetchPositions}) for stashing and * sorting distances to views being prefetched. */ private int[] mPrefetchDistances; + private final Runnable mCheckForGapsRunnable = this::checkForGaps; + /** * Constructor used when layout manager is set in XML by RecyclerView attribute * "layoutManager". Defaults to single column and vertical. */ @SuppressWarnings("unused") - public StaggeredGridLayoutManager(@NonNull Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { + public StaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); setOrientation(properties.orientation); setSpanCount(properties.spanCount); @@ -231,7 +263,7 @@ boolean checkForGaps() { if (getChildCount() == 0 || mGapStrategy == GAP_HANDLING_NONE || !isAttachedToWindow()) { return false; } - int minPos, maxPos; + final int minPos, maxPos; if (mShouldReverseLayout) { minPos = getLastChildPosition(); maxPos = getFirstChildPosition(); @@ -252,20 +284,20 @@ boolean checkForGaps() { return false; } int invalidGapDir = mShouldReverseLayout ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; - FullSpanItem invalidFsi = mLazySpanLookup + final FullSpanItem invalidFsi = mLazySpanLookup .getFirstFullSpanItemInRange(minPos, maxPos + 1, invalidGapDir, true); if (invalidFsi == null) { mLaidOutInvalidFullSpan = false; mLazySpanLookup.forceInvalidateAfter(maxPos + 1); return false; } - FullSpanItem validFsi = mLazySpanLookup - .getFirstFullSpanItemInRange(minPos, invalidFsi.getMPosition(), + final FullSpanItem validFsi = mLazySpanLookup + .getFirstFullSpanItemInRange(minPos, invalidFsi.mPosition, invalidGapDir * -1, true); if (validFsi == null) { - mLazySpanLookup.forceInvalidateAfter(invalidFsi.getMPosition()); + mLazySpanLookup.forceInvalidateAfter(invalidFsi.mPosition); } else { - mLazySpanLookup.forceInvalidateAfter(validFsi.getMPosition() + 1); + mLazySpanLookup.forceInvalidateAfter(validFsi.mPosition + 1); } requestSimpleAnimationsInNextLayout(); requestLayout(); @@ -280,7 +312,7 @@ public void onScrollStateChanged(int state) { } @Override - public void onDetachedFromWindow(@NonNull RecyclerView view, @NonNull RecyclerView.Recycler recycler) { + public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { super.onDetachedFromWindow(view, recycler); removeCallbacks(mCheckForGapsRunnable); @@ -302,8 +334,8 @@ View hasGapsToFix() { BitSet mSpansToCheck = new BitSet(mSpanCount); mSpansToCheck.set(0, mSpanCount, true); - int firstChildIndex, childLimit; - int preferredSpanDir = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1; + final int firstChildIndex, childLimit; + final int preferredSpanDir = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1; if (mShouldReverseLayout) { firstChildIndex = endChildIndex; @@ -312,7 +344,7 @@ View hasGapsToFix() { firstChildIndex = startChildIndex; childLimit = endChildIndex + 1; } - int nextChildDiff = firstChildIndex < childLimit ? 1 : -1; + final int nextChildDiff = firstChildIndex < childLimit ? 1 : -1; for (int i = firstChildIndex; i != childLimit; i += nextChildDiff) { View child = getChildAt(i); LayoutParams lp = (LayoutParams) child.getLayoutParams(); @@ -364,19 +396,85 @@ private boolean checkSpanForGap(Span span) { if (mShouldReverseLayout) { if (span.getEndLine() < mPrimaryOrientation.getEndAfterPadding()) { // if it is full span, it is OK - View endView = span.mViews.get(span.mViews.size() - 1); - LayoutParams lp = span.getLayoutParams(endView); + final View endView = span.mViews.get(span.mViews.size() - 1); + final LayoutParams lp = span.getLayoutParams(endView); return !lp.mFullSpan; } } else if (span.getStartLine() > mPrimaryOrientation.getStartAfterPadding()) { // if it is full span, it is OK - View startView = span.mViews.get(0); - LayoutParams lp = span.getLayoutParams(startView); + final View startView = span.mViews.get(0); + final LayoutParams lp = span.getLayoutParams(startView); return !lp.mFullSpan; } return false; } + /** + * Sets the number of spans for the layout. This will invalidate all of the span assignments + * for Views. + *

    + * Calling this method will automatically result in a new layout request unless the spanCount + * parameter is equal to current span count. + * + * @param spanCount Number of spans to layout + */ + public void setSpanCount(int spanCount) { + assertNotInLayoutOrScroll(null); + if (spanCount != mSpanCount) { + invalidateSpanAssignments(); + mSpanCount = spanCount; + mRemainingSpans = new BitSet(mSpanCount); + mSpans = new Span[mSpanCount]; + for (int i = 0; i < mSpanCount; i++) { + mSpans[i] = new Span(i); + } + requestLayout(); + } + } + + /** + * Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep + * scroll position if this method is called after views are laid out. + * + * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL && orientation != VERTICAL) { + throw new IllegalArgumentException("invalid orientation."); + } + assertNotInLayoutOrScroll(null); + if (orientation == mOrientation) { + return; + } + mOrientation = orientation; + OrientationHelper tmp = mPrimaryOrientation; + mPrimaryOrientation = mSecondaryOrientation; + mSecondaryOrientation = tmp; + requestLayout(); + } + + /** + * Sets whether LayoutManager should start laying out items from the end of the UI. The order + * items are traversed is not affected by this call. + *

    + * For vertical layout, if it is set to true, first item will be at the bottom of + * the list. + *

    + * For horizontal layouts, it depends on the layout direction. + * When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if + * {@link RecyclerView}} is RTL, it will layout from LTR. + * + * @param reverseLayout Whether layout should be in reverse or not + */ + public void setReverseLayout(boolean reverseLayout) { + assertNotInLayoutOrScroll(null); + if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) { + mPendingSavedState.mReverseLayout = reverseLayout; + } + mReverseLayout = reverseLayout; + requestLayout(); + } + /** * Returns the current gap handling strategy for StaggeredGridLayoutManager. *

    @@ -434,29 +532,6 @@ public int getSpanCount() { return mSpanCount; } - /** - * Sets the number of spans for the layout. This will invalidate all of the span assignments - * for Views. - *

    - * Calling this method will automatically result in a new layout request unless the spanCount - * parameter is equal to current span count. - * - * @param spanCount Number of spans to layout - */ - public void setSpanCount(int spanCount) { - assertNotInLayoutOrScroll(null); - if (spanCount != mSpanCount) { - invalidateSpanAssignments(); - mSpanCount = spanCount; - mRemainingSpans = new BitSet(mSpanCount); - mSpans = new Span[mSpanCount]; - for (int i = 0; i < mSpanCount; i++) { - mSpans[i] = new Span(i); - } - requestLayout(); - } - } - /** * For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items. *

    @@ -498,41 +573,19 @@ public boolean getReverseLayout() { return mReverseLayout; } - /** - * Sets whether LayoutManager should start laying out items from the end of the UI. The order - * items are traversed is not affected by this call. - *

    - * For vertical layout, if it is set to true, first item will be at the bottom of - * the list. - *

    - * For horizontal layouts, it depends on the layout direction. - * When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if - * {@link RecyclerView}} is RTL, it will layout from LTR. - * - * @param reverseLayout Whether layout should be in reverse or not - */ - public void setReverseLayout(boolean reverseLayout) { - assertNotInLayoutOrScroll(null); - if (mPendingSavedState != null && mPendingSavedState.getMReverseLayout() != reverseLayout) { - mPendingSavedState.setMReverseLayout(reverseLayout); - } - mReverseLayout = reverseLayout; - requestLayout(); - } - @Override public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { // we don't like it to wrap content in our non-scroll direction. - int width, height; - int horizontalPadding = getPaddingLeft() + getPaddingRight(); - int verticalPadding = getPaddingTop() + getPaddingBottom(); + final int width, height; + final int horizontalPadding = getPaddingLeft() + getPaddingRight(); + final int verticalPadding = getPaddingTop() + getPaddingBottom(); if (mOrientation == VERTICAL) { - int usedHeight = childrenBounds.height() + verticalPadding; + final int usedHeight = childrenBounds.height() + verticalPadding; height = chooseSize(hSpec, usedHeight, getMinimumHeight()); width = chooseSize(wSpec, mSizePerSpan * mSpanCount + horizontalPadding, getMinimumWidth()); } else { - int usedWidth = childrenBounds.width() + horizontalPadding; + final int usedWidth = childrenBounds.width() + horizontalPadding; width = chooseSize(wSpec, usedWidth, getMinimumWidth()); height = chooseSize(hSpec, mSizePerSpan * mSpanCount + verticalPadding, getMinimumHeight()); @@ -541,13 +594,13 @@ public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { } @Override - public void onLayoutChildren(@NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state) { + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { onLayoutChildren(recycler, state, true); } @Override public void onAdapterChanged(@Nullable RecyclerView.Adapter oldAdapter, - @Nullable RecyclerView.Adapter newAdapter) { + @Nullable RecyclerView.Adapter newAdapter) { // RV will remove all views so we should clear all spans and assignments of views into spans mLazySpanLookup.clear(); for (int i = 0; i < mSpanCount; i++) { @@ -556,8 +609,8 @@ public void onAdapterChanged(@Nullable RecyclerView.Adapter oldAdapter, } private void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state, - boolean shouldCheckForGaps) { - AnchorInfo anchorInfo = mAnchorInfo; + boolean shouldCheckForGaps) { + final AnchorInfo anchorInfo = mAnchorInfo; if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) { if (state.getItemCount() == 0) { removeAndRecycleAllViews(recycler); @@ -588,7 +641,7 @@ private void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State } if (getChildCount() > 0 && (mPendingSavedState == null - || mPendingSavedState.getMSpanOffsetsSize() < 1)) { + || mPendingSavedState.mSpanOffsetsSize < 1)) { if (anchorInfo.mInvalidateOffsets) { for (int i = 0; i < mSpanCount; i++) { // Scroll to position is set, clear. @@ -606,7 +659,7 @@ private void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State mAnchorInfo.saveSpanReferenceLines(mSpans); } else { for (int i = 0; i < mSpanCount; i++) { - Span span = mSpans[i]; + final Span span = mSpans[i]; span.clear(); span.setLine(mAnchorInfo.mSpanReferenceLines[i]); } @@ -624,15 +677,17 @@ private void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State fill(recycler, mLayoutState, state); // Layout end. setLayoutStateDirection(LayoutState.LAYOUT_END); + mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection; + fill(recycler, mLayoutState, state); } else { // Layout end. setLayoutStateDirection(LayoutState.LAYOUT_END); fill(recycler, mLayoutState, state); // Layout start. setLayoutStateDirection(LayoutState.LAYOUT_START); + mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection; + fill(recycler, mLayoutState, state); } - mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection; - fill(recycler, mLayoutState, state); repositionToWrapContentIfNecessary(); @@ -647,7 +702,7 @@ private void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State } boolean hasGaps = false; if (shouldCheckForGaps && !state.isPreLayout()) { - boolean needToCheckForGaps = mGapStrategy != GAP_HANDLING_NONE + final boolean needToCheckForGaps = mGapStrategy != GAP_HANDLING_NONE && getChildCount() > 0 && (mLaidOutInvalidFullSpan || hasGapsToFix() != null); if (needToCheckForGaps) { @@ -669,7 +724,7 @@ && getChildCount() > 0 } @Override - public void onLayoutCompleted(@NonNull RecyclerView.State state) { + public void onLayoutCompleted(RecyclerView.State state) { super.onLayoutCompleted(state); mPendingScrollPosition = RecyclerView.NO_POSITION; mPendingScrollPositionOffset = INVALID_OFFSET; @@ -682,7 +737,7 @@ private void repositionToWrapContentIfNecessary() { return; // nothing to do } float maxSize = 0; - int childCount = getChildCount(); + final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); float size = mSecondaryOrientation.getDecoratedMeasurement(child); @@ -691,7 +746,7 @@ private void repositionToWrapContentIfNecessary() { } LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); if (layoutParams.isFullSpan()) { - size = size / mSpanCount; + size = 1f * size / mSpanCount; } maxSize = Math.max(maxSize, size); } @@ -706,7 +761,7 @@ private void repositionToWrapContentIfNecessary() { } for (int i = 0; i < childCount; i++) { View child = getChildAt(i); - LayoutParams lp = (LayoutParams) child.getLayoutParams(); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp.mFullSpan) { continue; } @@ -730,13 +785,13 @@ private void applyPendingSavedState(AnchorInfo anchorInfo) { if (DEBUG) { Log.d(TAG, "found saved state: " + mPendingSavedState); } - if (mPendingSavedState.getMSpanOffsetsSize() > 0) { - if (mPendingSavedState.getMSpanOffsetsSize() == mSpanCount) { + if (mPendingSavedState.mSpanOffsetsSize > 0) { + if (mPendingSavedState.mSpanOffsetsSize == mSpanCount) { for (int i = 0; i < mSpanCount; i++) { mSpans[i].clear(); - int line = mPendingSavedState.getMSpanOffsets()[i]; + int line = mPendingSavedState.mSpanOffsets[i]; if (line != Span.INVALID_LINE) { - if (mPendingSavedState.getMAnchorLayoutFromEnd()) { + if (mPendingSavedState.mAnchorLayoutFromEnd) { line += mPrimaryOrientation.getEndAfterPadding(); } else { line += mPrimaryOrientation.getStartAfterPadding(); @@ -746,22 +801,22 @@ private void applyPendingSavedState(AnchorInfo anchorInfo) { } } else { mPendingSavedState.invalidateSpanInfo(); - mPendingSavedState.setMAnchorPosition(mPendingSavedState.getMVisibleAnchorPosition()); + mPendingSavedState.mAnchorPosition = mPendingSavedState.mVisibleAnchorPosition; } } - mLastLayoutRTL = mPendingSavedState.getMLastLayoutRTL(); - setReverseLayout(mPendingSavedState.getMReverseLayout()); + mLastLayoutRTL = mPendingSavedState.mLastLayoutRTL; + setReverseLayout(mPendingSavedState.mReverseLayout); resolveShouldLayoutReverse(); - if (mPendingSavedState.getMAnchorPosition() != RecyclerView.NO_POSITION) { - mPendingScrollPosition = mPendingSavedState.getMAnchorPosition(); - anchorInfo.mLayoutFromEnd = mPendingSavedState.getMAnchorLayoutFromEnd(); + if (mPendingSavedState.mAnchorPosition != RecyclerView.NO_POSITION) { + mPendingScrollPosition = mPendingSavedState.mAnchorPosition; + anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; } else { anchorInfo.mLayoutFromEnd = mShouldReverseLayout; } - if (mPendingSavedState.getMSpanLookupSize() > 1) { - mLazySpanLookup.mData = mPendingSavedState.getMSpanLookup(); - mLazySpanLookup.mFullSpanItems = mPendingSavedState.getMFullSpanItems(); + if (mPendingSavedState.mSpanLookupSize > 1) { + mLazySpanLookup.mData = mPendingSavedState.mSpanLookup; + mLazySpanLookup.mFullSpanItems = mPendingSavedState.mFullSpanItems; } } @@ -803,10 +858,10 @@ boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorI return false; } - if (mPendingSavedState == null || mPendingSavedState.getMAnchorPosition() == RecyclerView.NO_POSITION - || mPendingSavedState.getMSpanOffsetsSize() < 1) { + if (mPendingSavedState == null || mPendingSavedState.mAnchorPosition == RecyclerView.NO_POSITION + || mPendingSavedState.mSpanOffsetsSize < 1) { // If item is visible, make it fully visible. - View child = findViewByPosition(mPendingScrollPosition); + final View child = findViewByPosition(mPendingScrollPosition); if (child != null) { // Use regular anchor position, offset according to pending offset and target // child @@ -814,11 +869,11 @@ boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorI : getFirstChildPosition(); if (mPendingScrollPositionOffset != INVALID_OFFSET) { if (anchorInfo.mLayoutFromEnd) { - int target = mPrimaryOrientation.getEndAfterPadding() + final int target = mPrimaryOrientation.getEndAfterPadding() - mPendingScrollPositionOffset; anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedEnd(child); } else { - int target = mPrimaryOrientation.getStartAfterPadding() + final int target = mPrimaryOrientation.getStartAfterPadding() + mPendingScrollPositionOffset; anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedStart(child); } @@ -826,7 +881,7 @@ boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorI } // no offset provided. Decide according to the child location - int childSize = mPrimaryOrientation.getDecoratedMeasurement(child); + final int childSize = mPrimaryOrientation.getDecoratedMeasurement(child); if (childSize > mPrimaryOrientation.getTotalSpace()) { // Item does not fit. Fix depending on layout direction. anchorInfo.mOffset = anchorInfo.mLayoutFromEnd @@ -835,13 +890,13 @@ boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorI return true; } - int startGap = mPrimaryOrientation.getDecoratedStart(child) + final int startGap = mPrimaryOrientation.getDecoratedStart(child) - mPrimaryOrientation.getStartAfterPadding(); if (startGap < 0) { anchorInfo.mOffset = -startGap; return true; } - int endGap = mPrimaryOrientation.getEndAfterPadding() + final int endGap = mPrimaryOrientation.getEndAfterPadding() - mPrimaryOrientation.getDecoratedEnd(child); if (endGap < 0) { anchorInfo.mOffset = endGap; @@ -854,7 +909,7 @@ boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorI // child will be visible. anchorInfo.mPosition = mPendingScrollPosition; if (mPendingScrollPositionOffset == INVALID_OFFSET) { - int position = calculateScrollDirectionForPosition( + final int position = calculateScrollDirectionForPosition( anchorInfo.mPosition); anchorInfo.mLayoutFromEnd = position == LayoutState.LAYOUT_END; anchorInfo.assignCoordinateFromPadding(); @@ -1011,7 +1066,7 @@ public int[] findLastCompletelyVisibleItemPositions(int[] into) { } @Override - public int computeHorizontalScrollOffset(@NonNull RecyclerView.State state) { + public int computeHorizontalScrollOffset(RecyclerView.State state) { return computeScrollOffset(state); } @@ -1026,12 +1081,12 @@ private int computeScrollOffset(RecyclerView.State state) { } @Override - public int computeVerticalScrollOffset(@NonNull RecyclerView.State state) { + public int computeVerticalScrollOffset(RecyclerView.State state) { return computeScrollOffset(state); } @Override - public int computeHorizontalScrollExtent(@NonNull RecyclerView.State state) { + public int computeHorizontalScrollExtent(RecyclerView.State state) { return computeScrollExtent(state); } @@ -1046,12 +1101,12 @@ private int computeScrollExtent(RecyclerView.State state) { } @Override - public int computeVerticalScrollExtent(@NonNull RecyclerView.State state) { + public int computeVerticalScrollExtent(RecyclerView.State state) { return computeScrollExtent(state); } @Override - public int computeHorizontalScrollRange(@NonNull RecyclerView.State state) { + public int computeHorizontalScrollRange(RecyclerView.State state) { return computeScrollRange(state); } @@ -1066,12 +1121,12 @@ private int computeScrollRange(RecyclerView.State state) { } @Override - public int computeVerticalScrollRange(@NonNull RecyclerView.State state) { + public int computeVerticalScrollRange(RecyclerView.State state) { return computeScrollRange(state); } private void measureChildWithDecorationsAndMargin(View child, LayoutParams lp, - boolean alreadyMeasured) { + boolean alreadyMeasured) { if (lp.mFullSpan) { if (mOrientation == VERTICAL) { measureChildWithDecorationsAndMargin(child, mFullSizeSpec, @@ -1136,14 +1191,14 @@ private void measureChildWithDecorationsAndMargin(View child, LayoutParams lp, } private void measureChildWithDecorationsAndMargin(View child, int widthSpec, - int heightSpec, boolean alreadyMeasured) { + int heightSpec, boolean alreadyMeasured) { calculateItemDecorationsForChild(child, mTmpRect); LayoutParams lp = (LayoutParams) child.getLayoutParams(); widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mTmpRect.left, lp.rightMargin + mTmpRect.right); heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mTmpRect.top, lp.bottomMargin + mTmpRect.bottom); - boolean measure = alreadyMeasured + final boolean measure = alreadyMeasured ? shouldReMeasureChild(child, widthSpec, heightSpec, lp) : shouldMeasureChild(child, widthSpec, heightSpec, lp); if (measure) { @@ -1156,7 +1211,7 @@ private int updateSpecWithExtra(int spec, int startInset, int endInset) { if (startInset == 0 && endInset == 0) { return spec; } - int mode = View.MeasureSpec.getMode(spec); + final int mode = View.MeasureSpec.getMode(spec); if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) { return View.MeasureSpec.makeMeasureSpec( Math.max(0, View.MeasureSpec.getSize(spec) - startInset - endInset), mode); @@ -1165,7 +1220,7 @@ private int updateSpecWithExtra(int spec, int startInset, int endInset) { } @Override - public void onRestoreInstanceState(@NonNull Parcelable state) { + public void onRestoreInstanceState(Parcelable state) { if (state instanceof StaggeredGridLayoutManager_SavedState) { mPendingSavedState = (StaggeredGridLayoutManager_SavedState) state; if (mPendingScrollPosition != RecyclerView.NO_POSITION) { @@ -1184,23 +1239,24 @@ public Parcelable onSaveInstanceState() { return new StaggeredGridLayoutManager_SavedState(mPendingSavedState); } StaggeredGridLayoutManager_SavedState state = new StaggeredGridLayoutManager_SavedState(); - state.setMReverseLayout(mReverseLayout); - state.setMAnchorLayoutFromEnd(mLastLayoutFromEnd); - state.setMLastLayoutRTL(mLastLayoutRTL); + state.mReverseLayout = mReverseLayout; + state.mAnchorLayoutFromEnd = mLastLayoutFromEnd; + state.mLastLayoutRTL = mLastLayoutRTL; if (mLazySpanLookup != null && mLazySpanLookup.mData != null) { - state.setMSpanLookup(mLazySpanLookup.mData); - state.setMSpanLookupSize(state.getMSpanLookup().length); - state.setMFullSpanItems(mLazySpanLookup.mFullSpanItems); + state.mSpanLookup = mLazySpanLookup.mData; + state.mSpanLookupSize = state.mSpanLookup.length; + state.mFullSpanItems = mLazySpanLookup.mFullSpanItems; } else { - state.setMSpanLookupSize(0); + state.mSpanLookupSize = 0; } if (getChildCount() > 0) { - state.setMAnchorPosition(mLastLayoutFromEnd ? getLastChildPosition() : getFirstChildPosition()); - state.setMVisibleAnchorPosition(findFirstVisibleItemPositionInt()); - state.setMSpanOffsetsSize(mSpanCount); - state.setMSpanOffsets(new int[mSpanCount]); + state.mAnchorPosition = mLastLayoutFromEnd ? getLastChildPosition() + : getFirstChildPosition(); + state.mVisibleAnchorPosition = findFirstVisibleItemPositionInt(); + state.mSpanOffsetsSize = mSpanCount; + state.mSpanOffsets = new int[mSpanCount]; for (int i = 0; i < mSpanCount; i++) { int line; if (mLastLayoutFromEnd) { @@ -1214,12 +1270,12 @@ public Parcelable onSaveInstanceState() { line -= mPrimaryOrientation.getStartAfterPadding(); } } - state.getMSpanOffsets()[i] = line; + state.mSpanOffsets[i] = line; } } else { - state.setMAnchorPosition(RecyclerView.NO_POSITION); - state.setMVisibleAnchorPosition(RecyclerView.NO_POSITION); - state.setMSpanOffsetsSize(0); + state.mAnchorPosition = RecyclerView.NO_POSITION; + state.mVisibleAnchorPosition = RecyclerView.NO_POSITION; + state.mSpanOffsetsSize = 0; } if (DEBUG) { Log.d(TAG, "saved state:\n" + state); @@ -1228,16 +1284,46 @@ public Parcelable onSaveInstanceState() { } @Override - public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) { + public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler, + @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(recycler, state, info); + // Setting the classname allows accessibility services to set a role for staggered grids + // and ensures that they are treated distinctly from canonical grids with clear row/column + // semantics. + info.setClassName("androidx.recyclerview.widget.StaggeredGridLayoutManager"); + } + + @Override + public void onInitializeAccessibilityNodeInfoForItem(@NonNull RecyclerView.Recycler recycler, + @NonNull RecyclerView.State state, @NonNull View host, + @NonNull AccessibilityNodeInfoCompat info) { + ViewGroup.LayoutParams lp = host.getLayoutParams(); + if (!(lp instanceof LayoutParams)) { + super.onInitializeAccessibilityNodeInfoForItem(host, info); + return; + } + LayoutParams sglp = (LayoutParams) lp; + if (mOrientation == HORIZONTAL) { + info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( + sglp.getSpanIndex(), sglp.mFullSpan ? mSpanCount : 1, + -1, -1, false, false)); + } else { // VERTICAL + info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( + -1, -1, + sglp.getSpanIndex(), sglp.mFullSpan ? mSpanCount : 1, false, false)); + } + } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); if (getChildCount() > 0) { - View start = findFirstVisibleItemClosestToStart(false); - View end = findFirstVisibleItemClosestToEnd(false); + final View start = findFirstVisibleItemClosestToStart(false); + final View end = findFirstVisibleItemClosestToEnd(false); if (start == null || end == null) { return; } - int startPos = getPosition(start); - int endPos = getPosition(end); + final int startPos = getPosition(start); + final int endPos = getPosition(end); if (startPos < endPos) { event.setFromIndex(startPos); event.setToIndex(endPos); @@ -1254,11 +1340,29 @@ public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) { * of returning null. */ int findFirstVisibleItemPositionInt() { - View first = mShouldReverseLayout ? findFirstVisibleItemClosestToEnd(true) : + final View first = mShouldReverseLayout ? findFirstVisibleItemClosestToEnd(true) : findFirstVisibleItemClosestToStart(true); return first == null ? RecyclerView.NO_POSITION : getPosition(first); } + @Override + public int getRowCountForAccessibility(@NonNull RecyclerView.Recycler recycler, + @NonNull RecyclerView.State state) { + if (mOrientation == HORIZONTAL) { + return Math.min(mSpanCount, state.getItemCount()); + } + return -1; + } + + @Override + public int getColumnCountForAccessibility(@NonNull RecyclerView.Recycler recycler, + @NonNull RecyclerView.State state) { + if (mOrientation == VERTICAL) { + return Math.min(mSpanCount, state.getItemCount()); + } + return -1; + } + /** * This is for internal use. Not necessarily the child closest to start but the first child * we find that matches the criteria. @@ -1266,14 +1370,14 @@ int findFirstVisibleItemPositionInt() { * children order. */ View findFirstVisibleItemClosestToStart(boolean fullyVisible) { - int boundsStart = mPrimaryOrientation.getStartAfterPadding(); - int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); - int limit = getChildCount(); + final int boundsStart = mPrimaryOrientation.getStartAfterPadding(); + final int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); + final int limit = getChildCount(); View partiallyVisible = null; for (int i = 0; i < limit; i++) { - View child = getChildAt(i); - int childStart = mPrimaryOrientation.getDecoratedStart(child); - int childEnd = mPrimaryOrientation.getDecoratedEnd(child); + final View child = getChildAt(i); + final int childStart = mPrimaryOrientation.getDecoratedStart(child); + final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); if (childEnd <= boundsStart || childStart >= boundsEnd) { continue; // not visible at all } @@ -1296,13 +1400,13 @@ View findFirstVisibleItemClosestToStart(boolean fullyVisible) { * children order. */ View findFirstVisibleItemClosestToEnd(boolean fullyVisible) { - int boundsStart = mPrimaryOrientation.getStartAfterPadding(); - int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); + final int boundsStart = mPrimaryOrientation.getStartAfterPadding(); + final int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); View partiallyVisible = null; for (int i = getChildCount() - 1; i >= 0; i--) { - View child = getChildAt(i); - int childStart = mPrimaryOrientation.getDecoratedStart(child); - int childEnd = mPrimaryOrientation.getDecoratedEnd(child); + final View child = getChildAt(i); + final int childStart = mPrimaryOrientation.getDecoratedStart(child); + final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); if (childEnd <= boundsStart || childStart >= boundsEnd) { continue; // not visible at all } @@ -1319,8 +1423,8 @@ View findFirstVisibleItemClosestToEnd(boolean fullyVisible) { } private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state, - boolean canOffsetChildren) { - int maxEndLine = getMaxEnd(Integer.MIN_VALUE); + boolean canOffsetChildren) { + final int maxEndLine = getMaxEnd(Integer.MIN_VALUE); if (maxEndLine == Integer.MIN_VALUE) { return; } @@ -1338,8 +1442,8 @@ private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state, } private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state, - boolean canOffsetChildren) { - int minStartLine = getMinStart(Integer.MAX_VALUE); + boolean canOffsetChildren) { + final int minStartLine = getMinStart(Integer.MAX_VALUE); if (minStartLine == Integer.MAX_VALUE) { return; } @@ -1362,7 +1466,7 @@ private void updateLayoutState(int anchorPosition, RecyclerView.State state) { int startExtra = 0; int endExtra = 0; if (isSmoothScrolling()) { - int targetPos = state.getTargetScrollPosition(); + final int targetPos = state.getTargetScrollPosition(); if (targetPos != RecyclerView.NO_POSITION) { if (mShouldReverseLayout == targetPos < anchorPosition) { endExtra = mPrimaryOrientation.getTotalSpace(); @@ -1373,7 +1477,7 @@ private void updateLayoutState(int anchorPosition, RecyclerView.State state) { } // Line of the furthest row. - boolean clipToPadding = getClipToPadding(); + final boolean clipToPadding = getClipToPadding(); if (clipToPadding) { mLayoutState.mStartLine = mPrimaryOrientation.getStartAfterPadding() - startExtra; mLayoutState.mEndLine = mPrimaryOrientation.getEndAfterPadding() + endExtra; @@ -1410,29 +1514,29 @@ public void offsetChildrenVertical(int dy) { } @Override - public void onItemsRemoved(@NonNull RecyclerView recyclerView, int positionStart, int itemCount) { + public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.REMOVE); } @Override - public void onItemsAdded(@NonNull RecyclerView recyclerView, int positionStart, int itemCount) { + public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.ADD); } @Override - public void onItemsChanged(@NonNull RecyclerView recyclerView) { + public void onItemsChanged(RecyclerView recyclerView) { mLazySpanLookup.clear(); requestLayout(); } @Override - public void onItemsMoved(@NonNull RecyclerView recyclerView, int from, int to, int itemCount) { + public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { handleUpdate(from, to, AdapterHelper.UpdateOp.MOVE); } @Override - public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart, int itemCount, - Object payload) { + public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, + Object payload) { handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.UPDATE); } @@ -1441,8 +1545,8 @@ public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart */ private void handleUpdate(int positionStart, int itemCountOrToPosition, int cmd) { int minPosition = mShouldReverseLayout ? getLastChildPosition() : getFirstChildPosition(); - int affectedRangeEnd; // exclusive - int affectedRangeStart; // inclusive + final int affectedRangeEnd; // exclusive + final int affectedRangeStart; // inclusive if (cmd == AdapterHelper.UpdateOp.MOVE) { if (positionStart < itemCountOrToPosition) { @@ -1483,10 +1587,10 @@ private void handleUpdate(int positionStart, int itemCountOrToPosition, int cmd) } private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, - RecyclerView.State state) { + RecyclerView.State state) { mRemainingSpans.set(0, mSpanCount, true); // The target position we are trying to reach. - int targetLine; + final int targetLine; // Line of the furthest row. if (mLayoutState.mInfinite) { @@ -1510,7 +1614,7 @@ private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, } // the default coordinate to add new view. - int defaultNewViewLine = mShouldReverseLayout + final int defaultNewViewLine = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding() : mPrimaryOrientation.getStartAfterPadding(); boolean added = false; @@ -1518,10 +1622,10 @@ private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, && (mLayoutState.mInfinite || !mRemainingSpans.isEmpty())) { View view = layoutState.next(recycler); LayoutParams lp = ((LayoutParams) view.getLayoutParams()); - int position = lp.getViewLayoutPosition(); - int spanIndex = mLazySpanLookup.getSpan(position); + final int position = lp.getViewLayoutPosition(); + final int spanIndex = mLazySpanLookup.getSpan(position); Span currentSpan; - boolean assignSpan = spanIndex == LayoutParams.INVALID_SPAN_ID; + final boolean assignSpan = spanIndex == LayoutParams.INVALID_SPAN_ID; if (assignSpan) { currentSpan = lp.mFullSpan ? mSpans[0] : getNextSpan(layoutState); mLazySpanLookup.setSpan(position, currentSpan); @@ -1543,8 +1647,8 @@ private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, } measureChildWithDecorationsAndMargin(view, lp, false); - int start; - int end; + final int start; + final int end; if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { start = lp.mFullSpan ? getMaxEnd(defaultNewViewLine) : currentSpan.getEndLine(defaultNewViewLine); @@ -1552,8 +1656,8 @@ private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, if (assignSpan && lp.mFullSpan) { FullSpanItem fullSpanItem; fullSpanItem = createFullSpanItemFromEnd(start); - fullSpanItem.setMGapDir(LayoutState.LAYOUT_START); - fullSpanItem.setMPosition(position); + fullSpanItem.mGapDir = LayoutState.LAYOUT_START; + fullSpanItem.mPosition = position; mLazySpanLookup.addFullSpanItem(fullSpanItem); } } else { @@ -1563,8 +1667,8 @@ private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, if (assignSpan && lp.mFullSpan) { FullSpanItem fullSpanItem; fullSpanItem = createFullSpanItemFromStart(end); - fullSpanItem.setMGapDir(LayoutState.LAYOUT_END); - fullSpanItem.setMPosition(position); + fullSpanItem.mGapDir = LayoutState.LAYOUT_END; + fullSpanItem.mPosition = position; mLazySpanLookup.addFullSpanItem(fullSpanItem); } } @@ -1574,25 +1678,25 @@ private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, if (assignSpan) { mLaidOutInvalidFullSpan = true; } else { - boolean hasInvalidGap; + final boolean hasInvalidGap; if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { hasInvalidGap = !areAllEndsEqual(); } else { // layoutState.mLayoutDirection == LAYOUT_START hasInvalidGap = !areAllStartsEqual(); } if (hasInvalidGap) { - FullSpanItem fullSpanItem = mLazySpanLookup + final FullSpanItem fullSpanItem = mLazySpanLookup .getFullSpanItem(position); if (fullSpanItem != null) { - fullSpanItem.setMHasUnwantedGapAfter(true); + fullSpanItem.mHasUnwantedGapAfter = true; } mLaidOutInvalidFullSpan = true; } } } attachViewToSpans(view, lp, layoutState); - int otherStart; - int otherEnd; + final int otherStart; + final int otherEnd; if (isLayoutRTL() && mOrientation == VERTICAL) { otherEnd = lp.mFullSpan ? mSecondaryOrientation.getEndAfterPadding() : mSecondaryOrientation.getEndAfterPadding() @@ -1601,7 +1705,7 @@ private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, } else { otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding() : currentSpan.mIndex * mSizePerSpan - + mSecondaryOrientation.getStartAfterPadding(); + + mSecondaryOrientation.getStartAfterPadding(); otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view); } @@ -1629,12 +1733,12 @@ private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, if (!added) { recycle(recycler, mLayoutState); } - int diff; + final int diff; if (mLayoutState.mLayoutDirection == LayoutState.LAYOUT_START) { - int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding()); + final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding()); diff = mPrimaryOrientation.getStartAfterPadding() - minStart; } else { - int maxEnd = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); + final int maxEnd = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); diff = maxEnd - mPrimaryOrientation.getEndAfterPadding(); } return diff > 0 ? Math.min(layoutState.mAvailable, diff) : 0; @@ -1642,18 +1746,18 @@ private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, private FullSpanItem createFullSpanItemFromEnd(int newItemTop) { FullSpanItem fsi = new FullSpanItem(); - fsi.setMGapPerSpan(new int[mSpanCount]); + fsi.mGapPerSpan = new int[mSpanCount]; for (int i = 0; i < mSpanCount; i++) { - fsi.getMGapPerSpan()[i] = newItemTop - mSpans[i].getEndLine(newItemTop); + fsi.mGapPerSpan[i] = newItemTop - mSpans[i].getEndLine(newItemTop); } return fsi; } private FullSpanItem createFullSpanItemFromStart(int newItemBottom) { FullSpanItem fsi = new FullSpanItem(); - fsi.setMGapPerSpan(new int[mSpanCount]); + fsi.mGapPerSpan = new int[mSpanCount]; for (int i = 0; i < mSpanCount; i++) { - fsi.getMGapPerSpan()[i] = mSpans[i].getStartLine(newItemBottom) - newItemBottom; + fsi.mGapPerSpan[i] = mSpans[i].getStartLine(newItemBottom) - newItemBottom; } return fsi; } @@ -1691,7 +1795,7 @@ private void recycle(RecyclerView.Recycler recycler, LayoutState layoutState) { if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { // calculate recycle line int scrolled = layoutState.mStartLine - getMaxStart(layoutState.mStartLine); - int line; + final int line; if (scrolled < 0) { line = layoutState.mEndLine; } else { @@ -1701,7 +1805,7 @@ private void recycle(RecyclerView.Recycler recycler, LayoutState layoutState) { } else { // calculate recycle line int scrolled = getMinEnd(layoutState.mEndLine) - layoutState.mEndLine; - int line; + final int line; if (scrolled < 0) { line = layoutState.mStartLine; } else { @@ -1737,14 +1841,14 @@ private void updateAllRemainingSpans(int layoutDir, int targetLine) { } private void updateRemainingSpans(Span span, int layoutDir, int targetLine) { - int deletedSize = span.getDeletedSize(); + final int deletedSize = span.getDeletedSize(); if (layoutDir == LayoutState.LAYOUT_START) { - int line = span.getStartLine(); + final int line = span.getStartLine(); if (line + deletedSize <= targetLine) { mRemainingSpans.set(span.mIndex, false); } } else { - int line = span.getEndLine(); + final int line = span.getEndLine(); if (line - deletedSize >= targetLine) { mRemainingSpans.set(span.mIndex, false); } @@ -1754,7 +1858,7 @@ private void updateRemainingSpans(Span span, int layoutDir, int targetLine) { private int getMaxStart(int def) { int maxStart = mSpans[0].getStartLine(def); for (int i = 1; i < mSpanCount; i++) { - int spanStart = mSpans[i].getStartLine(def); + final int spanStart = mSpans[i].getStartLine(def); if (spanStart > maxStart) { maxStart = spanStart; } @@ -1765,7 +1869,7 @@ private int getMaxStart(int def) { private int getMinStart(int def) { int minStart = mSpans[0].getStartLine(def); for (int i = 1; i < mSpanCount; i++) { - int spanStart = mSpans[i].getStartLine(def); + final int spanStart = mSpans[i].getStartLine(def); if (spanStart < minStart) { minStart = spanStart; } @@ -1796,7 +1900,7 @@ boolean areAllStartsEqual() { private int getMaxEnd(int def) { int maxEnd = mSpans[0].getEndLine(def); for (int i = 1; i < mSpanCount; i++) { - int spanEnd = mSpans[i].getEndLine(def); + final int spanEnd = mSpans[i].getEndLine(def); if (spanEnd > maxEnd) { maxEnd = spanEnd; } @@ -1807,7 +1911,7 @@ private int getMaxEnd(int def) { private int getMinEnd(int def) { int minEnd = mSpans[0].getEndLine(def); for (int i = 1; i < mSpanCount; i++) { - int spanEnd = mSpans[i].getEndLine(def); + final int spanEnd = mSpans[i].getEndLine(def); if (spanEnd < minEnd) { minEnd = spanEnd; } @@ -1845,7 +1949,7 @@ private void recycleFromStart(RecyclerView.Recycler recycler, int line) { } private void recycleFromEnd(RecyclerView.Recycler recycler, int line) { - int childCount = getChildCount(); + final int childCount = getChildCount(); int i; for (i = childCount - 1; i >= 0; i--) { View child = getChildAt(i); @@ -1889,8 +1993,8 @@ private boolean preferLastSpan(int layoutDir) { * Finds the span for the next view. */ private Span getNextSpan(LayoutState layoutState) { - boolean preferLastSpan = preferLastSpan(layoutState.mLayoutDirection); - int startIndex, endIndex, diff; + final boolean preferLastSpan = preferLastSpan(layoutState.mLayoutDirection); + final int startIndex, endIndex, diff; if (preferLastSpan) { startIndex = mSpanCount - 1; endIndex = -1; @@ -1903,9 +2007,9 @@ private Span getNextSpan(LayoutState layoutState) { if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { Span min = null; int minLine = Integer.MAX_VALUE; - int defaultLine = mPrimaryOrientation.getStartAfterPadding(); + final int defaultLine = mPrimaryOrientation.getStartAfterPadding(); for (int i = startIndex; i != endIndex; i += diff) { - Span other = mSpans[i]; + final Span other = mSpans[i]; int otherLine = other.getEndLine(defaultLine); if (otherLine < minLine) { min = other; @@ -1916,9 +2020,9 @@ private Span getNextSpan(LayoutState layoutState) { } else { Span max = null; int maxLine = Integer.MIN_VALUE; - int defaultLine = mPrimaryOrientation.getEndAfterPadding(); + final int defaultLine = mPrimaryOrientation.getEndAfterPadding(); for (int i = startIndex; i != endIndex; i += diff) { - Span other = mSpans[i]; + final Span other = mSpans[i]; int otherLine = other.getStartLine(defaultLine); if (otherLine > maxLine) { max = other; @@ -1940,14 +2044,14 @@ public boolean canScrollHorizontally() { } @Override - public int scrollHorizontallyBy(int dx, @NonNull RecyclerView.Recycler recycler, - @NonNull RecyclerView.State state) { + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, + RecyclerView.State state) { return scrollBy(dx, recycler, state); } @Override - public int scrollVerticallyBy(int dy, @NonNull RecyclerView.Recycler recycler, - @NonNull RecyclerView.State state) { + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, + RecyclerView.State state) { return scrollBy(dy, recycler, state); } @@ -1955,13 +2059,13 @@ private int calculateScrollDirectionForPosition(int position) { if (getChildCount() == 0) { return mShouldReverseLayout ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; } - int firstChildPos = getFirstChildPosition(); + final int firstChildPos = getFirstChildPosition(); return position < firstChildPos != mShouldReverseLayout ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; } @Override public PointF computeScrollVectorForPosition(int targetPosition) { - int direction = calculateScrollDirectionForPosition(targetPosition); + final int direction = calculateScrollDirectionForPosition(targetPosition); PointF outVector = new PointF(); if (direction == 0) { return null; @@ -1977,8 +2081,8 @@ public PointF computeScrollVectorForPosition(int targetPosition) { } @Override - public void smoothScrollToPosition(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.State state, - int position) { + public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, + int position) { LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()); scroller.setTargetPosition(position); startSmoothScroll(scroller); @@ -1986,7 +2090,7 @@ public void smoothScrollToPosition(@NonNull RecyclerView recyclerView, @NonNull @Override public void scrollToPosition(int position) { - if (mPendingSavedState != null && mPendingSavedState.getMAnchorPosition() != position) { + if (mPendingSavedState != null && mPendingSavedState.mAnchorPosition != position) { mPendingSavedState.invalidateAnchorPositionInfo(); } mPendingScrollPosition = position; @@ -2016,13 +2120,11 @@ public void scrollToPositionWithOffset(int position, int offset) { requestLayout(); } - /** - * @hide - */ + /** @hide */ @Override @RestrictTo(LIBRARY) - public void collectAdjacentPrefetchPositions(int dx, int dy, @NonNull RecyclerView.State state, - @NonNull LayoutPrefetchRegistry layoutPrefetchRegistry) { + public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, + LayoutPrefetchRegistry layoutPrefetchRegistry) { /* This method uses the simplifying assumption that the next N items (where N = span count) * will be assigned, one-to-one, to spans, where ordering is based on which span extends * least beyond the viewport. @@ -2069,8 +2171,8 @@ public void collectAdjacentPrefetchPositions(int dx, int dy, @NonNull RecyclerVi } void prepareLayoutStateForDelta(int delta, RecyclerView.State state) { - int referenceChildPosition; - int layoutDir; + final int referenceChildPosition; + final int layoutDir; if (delta > 0) { // layout towards end layoutDir = LayoutState.LAYOUT_END; referenceChildPosition = getLastChildPosition(); @@ -2092,8 +2194,8 @@ int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { prepareLayoutStateForDelta(dt, state); int consumed = fill(recycler, mLayoutState, state); - int available = mLayoutState.mAvailable; - int totalScroll; + final int available = mLayoutState.mAvailable; + final int totalScroll; if (available < consumed) { totalScroll = dt; } else if (dt < 0) { @@ -2114,12 +2216,12 @@ int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { } int getLastChildPosition() { - int childCount = getChildCount(); + final int childCount = getChildCount(); return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1)); } int getFirstChildPosition() { - int childCount = getChildCount(); + final int childCount = getChildCount(); return childCount == 0 ? 0 : getPosition(getChildAt(0)); } @@ -2129,10 +2231,10 @@ int getFirstChildPosition() { * @return Position of the View or 0 if it cannot find any such View. */ private int findFirstReferenceChildPosition(int itemCount) { - int limit = getChildCount(); + final int limit = getChildCount(); for (int i = 0; i < limit; i++) { - View view = getChildAt(i); - int position = getPosition(view); + final View view = getChildAt(i); + final int position = getPosition(view); if (position >= 0 && position < itemCount) { return position; } @@ -2147,8 +2249,8 @@ private int findFirstReferenceChildPosition(int itemCount) { */ private int findLastReferenceChildPosition(int itemCount) { for (int i = getChildCount() - 1; i >= 0; i--) { - View view = getChildAt(i); - int position = getPosition(view); + final View view = getChildAt(i); + final int position = getPosition(view); if (position >= 0 && position < itemCount) { return position; } @@ -2156,7 +2258,7 @@ private int findLastReferenceChildPosition(int itemCount) { return 0; } - @NonNull + @SuppressWarnings("deprecation") @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { if (mOrientation == HORIZONTAL) { @@ -2191,49 +2293,28 @@ public int getOrientation() { return mOrientation; } - /** - * Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep - * scroll position if this method is called after views are laid out. - * - * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} - */ - public void setOrientation(int orientation) { - if (orientation != HORIZONTAL && orientation != VERTICAL) { - throw new IllegalArgumentException("invalid orientation."); - } - assertNotInLayoutOrScroll(null); - if (orientation == mOrientation) { - return; - } - mOrientation = orientation; - OrientationHelper tmp = mPrimaryOrientation; - mPrimaryOrientation = mSecondaryOrientation; - mSecondaryOrientation = tmp; - requestLayout(); - } - @Nullable @Override - public View onFocusSearchFailed(@NonNull View focused, int direction, @NonNull RecyclerView.Recycler recycler, - @NonNull RecyclerView.State state) { + public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, + RecyclerView.State state) { if (getChildCount() == 0) { return null; } - View directChild = findContainingItemView(focused); + final View directChild = findContainingItemView(focused); if (directChild == null) { return null; } resolveShouldLayoutReverse(); - int layoutDir = convertFocusDirectionToLayoutDirection(direction); + final int layoutDir = convertFocusDirectionToLayoutDirection(direction); if (layoutDir == LayoutState.INVALID_LAYOUT) { return null; } LayoutParams prevFocusLayoutParams = (LayoutParams) directChild.getLayoutParams(); boolean prevFocusFullSpan = prevFocusLayoutParams.mFullSpan; - Span prevFocusSpan = prevFocusLayoutParams.mSpan; - int referenceChildPosition; + final Span prevFocusSpan = prevFocusLayoutParams.mSpan; + final int referenceChildPosition; if (layoutDir == LayoutState.LAYOUT_END) { // layout towards end referenceChildPosition = getLastChildPosition(); } else { @@ -2278,7 +2359,7 @@ public View onFocusSearchFailed(@NonNull View focused, int direction, @NonNull R // is done in the same fashion: first, check the views in the desired span and if no // candidate is found, traverse the views in all the remaining spans. boolean shouldSearchFromStart = !mReverseLayout == (layoutDir == LayoutState.LAYOUT_START); - View unfocusableCandidate; + View unfocusableCandidate = null; if (!prevFocusFullSpan) { unfocusableCandidate = findViewByPosition(shouldSearchFromStart ? prevFocusSpan.findFirstPartiallyVisibleItemPosition() : @@ -2402,25 +2483,25 @@ public LayoutParams(RecyclerView.LayoutParams source) { } /** - * Returns whether this View occupies all available spans or just one. + * When set to true, the item will layout using all span area. That means, if orientation + * is vertical, the view will have full width; if orientation is horizontal, the view will + * have full height. * - * @return True if the View occupies all spans or false otherwise. - * @see #setFullSpan(boolean) + * @param fullSpan True if this item should traverse all spans. + * @see #isFullSpan() */ - public boolean isFullSpan() { - return mFullSpan; + public void setFullSpan(boolean fullSpan) { + mFullSpan = fullSpan; } /** - * When set to true, the item will layout using all span area. That means, if orientation - * is vertical, the view will have full width; if orientation is horizontal, the view will - * have full height. + * Returns whether this View occupies all available spans or just one. * - * @param fullSpan True if this item should traverse all spans. - * @see #isFullSpan() + * @return True if the View occupies all spans or false otherwise. + * @see #setFullSpan(boolean) */ - public void setFullSpan(boolean fullSpan) { - mFullSpan = fullSpan; + public boolean isFullSpan() { + return mFullSpan; } /** @@ -2437,253 +2518,15 @@ public final int getSpanIndex() { } } - /** - * An array of mappings from adapter position to span. - * This only grows when a write happens and it grows up to the size of the adapter. - */ - static class LazySpanLookup { - - private static final int MIN_SIZE = 10; - int[] mData; - ArrayList mFullSpanItems; - - - /** - * Invalidates everything after this position, including full span information - */ - int forceInvalidateAfter(int position) { - if (mFullSpanItems != null) { - for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { - FullSpanItem fsi = mFullSpanItems.get(i); - if (fsi.getMPosition() >= position) { - mFullSpanItems.remove(i); - } - } - } - return invalidateAfter(position); - } - - /** - * returns end position for invalidation. - */ - int invalidateAfter(int position) { - if (mData == null) { - return RecyclerView.NO_POSITION; - } - if (position >= mData.length) { - return RecyclerView.NO_POSITION; - } - int endPosition = invalidateFullSpansAfter(position); - if (endPosition == RecyclerView.NO_POSITION) { - Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID); - return mData.length; - } else { - // Just invalidate items in between `position` and the next full span item, or the - // end of the tracked spans in mData if it's not been lengthened yet. - int invalidateToIndex = Math.min(endPosition + 1, mData.length); - Arrays.fill(mData, position, invalidateToIndex, LayoutParams.INVALID_SPAN_ID); - return invalidateToIndex; - } - } - - int getSpan(int position) { - if (mData == null || position >= mData.length) { - return LayoutParams.INVALID_SPAN_ID; - } else { - return mData[position]; - } - } - - void setSpan(int position, Span span) { - ensureSize(position); - mData[position] = span.mIndex; - } - - int sizeForPosition(int position) { - int len = mData.length; - while (len <= position) { - len *= 2; - } - return len; - } - - void ensureSize(int position) { - if (mData == null) { - mData = new int[Math.max(position, MIN_SIZE) + 1]; - Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); - } else if (position >= mData.length) { - int[] old = mData; - mData = new int[sizeForPosition(position)]; - System.arraycopy(old, 0, mData, 0, old.length); - Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID); - } - } - - void clear() { - if (mData != null) { - Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); - } - mFullSpanItems = null; - } - - void offsetForRemoval(int positionStart, int itemCount) { - if (mData == null || positionStart >= mData.length) { - return; - } - ensureSize(positionStart + itemCount); - System.arraycopy(mData, positionStart + itemCount, mData, positionStart, - mData.length - positionStart - itemCount); - Arrays.fill(mData, mData.length - itemCount, mData.length, - LayoutParams.INVALID_SPAN_ID); - offsetFullSpansForRemoval(positionStart, itemCount); - } - - private void offsetFullSpansForRemoval(int positionStart, int itemCount) { - if (mFullSpanItems == null) { - return; - } - int end = positionStart + itemCount; - for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { - FullSpanItem fsi = mFullSpanItems.get(i); - if (fsi.getMPosition() < positionStart) { - continue; - } - if (fsi.getMPosition() < end) { - mFullSpanItems.remove(i); - } else { - fsi.setMPosition(fsi.getMPosition() - itemCount); - } - } - } - - void offsetForAddition(int positionStart, int itemCount) { - if (mData == null || positionStart >= mData.length) { - return; - } - ensureSize(positionStart + itemCount); - System.arraycopy(mData, positionStart, mData, positionStart + itemCount, - mData.length - positionStart - itemCount); - Arrays.fill(mData, positionStart, positionStart + itemCount, - LayoutParams.INVALID_SPAN_ID); - offsetFullSpansForAddition(positionStart, itemCount); - } - - private void offsetFullSpansForAddition(int positionStart, int itemCount) { - if (mFullSpanItems == null) { - return; - } - for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { - FullSpanItem fsi = mFullSpanItems.get(i); - if (fsi.getMPosition() < positionStart) { - continue; - } - fsi.setMPosition(fsi.getMPosition() + itemCount); - } - } - - /** - * Returns when invalidation should end. e.g. hitting a full span position. - * Returned position SHOULD BE invalidated. - */ - private int invalidateFullSpansAfter(int position) { - if (mFullSpanItems == null) { - return RecyclerView.NO_POSITION; - } - FullSpanItem item = getFullSpanItem(position); - // if there is an fsi at this position, get rid of it. - if (item != null) { - mFullSpanItems.remove(item); - } - int nextFsiIndex = -1; - int count = mFullSpanItems.size(); - for (int i = 0; i < count; i++) { - FullSpanItem fsi = mFullSpanItems.get(i); - if (fsi.getMPosition() >= position) { - nextFsiIndex = i; - break; - } - } - if (nextFsiIndex != -1) { - FullSpanItem fsi = mFullSpanItems.get(nextFsiIndex); - mFullSpanItems.remove(nextFsiIndex); - return fsi.getMPosition(); - } - return RecyclerView.NO_POSITION; - } - - public void addFullSpanItem(FullSpanItem fullSpanItem) { - if (mFullSpanItems == null) { - mFullSpanItems = new ArrayList<>(); - } - int size = mFullSpanItems.size(); - for (int i = 0; i < size; i++) { - FullSpanItem other = mFullSpanItems.get(i); - if (other.getMPosition() == fullSpanItem.getMPosition()) { - if (DEBUG) { - throw new IllegalStateException("two fsis for same position"); - } else { - mFullSpanItems.remove(i); - } - } - if (other.getMPosition() >= fullSpanItem.getMPosition()) { - mFullSpanItems.add(i, fullSpanItem); - return; - } - } - // if it is not added to a position. - mFullSpanItems.add(fullSpanItem); - } - - public FullSpanItem getFullSpanItem(int position) { - if (mFullSpanItems == null) { - return null; - } - for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { - FullSpanItem fsi = mFullSpanItems.get(i); - if (fsi.getMPosition() == position) { - return fsi; - } - } - return null; - } - - /** - * @param minPos inclusive - * @param maxPos exclusive - * @param gapDir if not 0, returns FSIs on in that direction - * @param hasUnwantedGapAfter If true, when full span item has unwanted gaps, it will be - * returned even if its gap direction does not match. - */ - public FullSpanItem getFirstFullSpanItemInRange(int minPos, int maxPos, int gapDir, - boolean hasUnwantedGapAfter) { - if (mFullSpanItems == null) { - return null; - } - int limit = mFullSpanItems.size(); - for (int i = 0; i < limit; i++) { - FullSpanItem fsi = mFullSpanItems.get(i); - if (fsi.getMPosition() >= maxPos) { - return null; - } - if (fsi.getMPosition() >= minPos - && (gapDir == 0 || fsi.getMGapDir() == gapDir - || (hasUnwantedGapAfter && fsi.getMHasUnwantedGapAfter()))) { - return fsi; - } - } - return null; - } - } - // Package scoped to access from tests. class Span { static final int INVALID_LINE = Integer.MIN_VALUE; - final int mIndex; - final ArrayList mViews = new ArrayList<>(); + ArrayList mViews = new ArrayList<>(); int mCachedStart = INVALID_LINE; int mCachedEnd = INVALID_LINE; - int mDeletedSize; + int mDeletedSize = 0; + final int mIndex; Span(int index) { mIndex = index; @@ -2701,13 +2544,13 @@ int getStartLine(int def) { } void calculateCachedStart() { - View startView = mViews.get(0); - LayoutParams lp = getLayoutParams(startView); + final View startView = mViews.get(0); + final LayoutParams lp = getLayoutParams(startView); mCachedStart = mPrimaryOrientation.getDecoratedStart(startView); if (lp.mFullSpan) { FullSpanItem fsi = mLazySpanLookup .getFullSpanItem(lp.getViewLayoutPosition()); - if (fsi != null && fsi.getMGapDir() == LayoutState.LAYOUT_START) { + if (fsi != null && fsi.mGapDir == LayoutState.LAYOUT_START) { mCachedStart -= fsi.getGapForSpan(mIndex); } } @@ -2726,7 +2569,7 @@ int getEndLine(int def) { if (mCachedEnd != INVALID_LINE) { return mCachedEnd; } - int size = mViews.size(); + final int size = mViews.size(); if (size == 0) { return def; } @@ -2735,13 +2578,13 @@ int getEndLine(int def) { } void calculateCachedEnd() { - View endView = mViews.get(mViews.size() - 1); - LayoutParams lp = getLayoutParams(endView); + final View endView = mViews.get(mViews.size() - 1); + final LayoutParams lp = getLayoutParams(endView); mCachedEnd = mPrimaryOrientation.getDecoratedEnd(endView); if (lp.mFullSpan) { FullSpanItem fsi = mLazySpanLookup .getFullSpanItem(lp.getViewLayoutPosition()); - if (fsi != null && fsi.getMGapDir() == LayoutState.LAYOUT_END) { + if (fsi != null && fsi.mGapDir == LayoutState.LAYOUT_END) { mCachedEnd += fsi.getGapForSpan(mIndex); } } @@ -2794,7 +2637,8 @@ void cacheReferenceLineAndClear(boolean reverseLayout, int offset) { if (reference == INVALID_LINE) { return; } - if (reverseLayout ? reference < mPrimaryOrientation.getEndAfterPadding() : reference > mPrimaryOrientation.getStartAfterPadding()) { + if ((reverseLayout && reference < mPrimaryOrientation.getEndAfterPadding()) + || (!reverseLayout && reference > mPrimaryOrientation.getStartAfterPadding())) { return; } if (offset != INVALID_OFFSET) { @@ -2819,9 +2663,9 @@ void setLine(int line) { } void popEnd() { - int size = mViews.size(); + final int size = mViews.size(); View end = mViews.remove(size - 1); - LayoutParams lp = getLayoutParams(end); + final LayoutParams lp = getLayoutParams(end); lp.mSpan = null; if (lp.isItemRemoved() || lp.isItemChanged()) { mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end); @@ -2834,7 +2678,7 @@ void popEnd() { void popStart() { View start = mViews.remove(0); - LayoutParams lp = getLayoutParams(start); + final LayoutParams lp = getLayoutParams(start); lp.mSpan = null; if (mViews.size() == 0) { mCachedEnd = INVALID_LINE; @@ -2908,11 +2752,10 @@ public int findLastCompletelyVisibleItemPosition() { * area. This is used e.g. inside * {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)} for * calculating the next unfocusable child to become visible on the screen. - * - * @param fromIndex The child position index to start the search from. - * @param toIndex The child position index to end the search at. - * @param completelyVisible True if we have to only consider completely visible views, - * false otherwise. + * @param fromIndex The child position index to start the search from. + * @param toIndex The child position index to end the search at. + * @param completelyVisible True if we have to only consider completely visible views, + * false otherwise. * @param acceptCompletelyVisible True if we can consider both partially or fully visible * views, false, if only a partially visible child should be * returned. @@ -2923,16 +2766,16 @@ public int findLastCompletelyVisibleItemPosition() { * {@link RecyclerView#NO_POSITION} if no such view is found. */ int findOnePartiallyOrCompletelyVisibleChild(int fromIndex, int toIndex, - boolean completelyVisible, - boolean acceptCompletelyVisible, - boolean acceptEndPointInclusion) { - int start = mPrimaryOrientation.getStartAfterPadding(); - int end = mPrimaryOrientation.getEndAfterPadding(); - int next = toIndex > fromIndex ? 1 : -1; + boolean completelyVisible, + boolean acceptCompletelyVisible, + boolean acceptEndPointInclusion) { + final int start = mPrimaryOrientation.getStartAfterPadding(); + final int end = mPrimaryOrientation.getEndAfterPadding(); + final int next = toIndex > fromIndex ? 1 : -1; for (int i = fromIndex; i != toIndex; i += next) { - View child = mViews.get(i); - int childStart = mPrimaryOrientation.getDecoratedStart(child); - int childEnd = mPrimaryOrientation.getDecoratedEnd(child); + final View child = mViews.get(i); + final int childStart = mPrimaryOrientation.getDecoratedStart(child); + final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); boolean childStartInclusion = acceptEndPointInclusion ? (childStart <= end) : (childStart < end); boolean childEndInclusion = acceptEndPointInclusion ? (childEnd >= start) @@ -2962,7 +2805,7 @@ int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) { } int findOnePartiallyVisibleChild(int fromIndex, int toIndex, - boolean acceptEndPointInclusion) { + boolean acceptEndPointInclusion) { return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, false, false, acceptEndPointInclusion); } @@ -2973,10 +2816,11 @@ int findOnePartiallyVisibleChild(int fromIndex, int toIndex, public View getFocusableViewAfter(int referenceChildPosition, int layoutDir) { View candidate = null; if (layoutDir == LayoutState.LAYOUT_START) { - int limit = mViews.size(); + final int limit = mViews.size(); for (int i = 0; i < limit; i++) { - View view = mViews.get(i); - if (mReverseLayout ? getPosition(view) <= referenceChildPosition : getPosition(view) >= referenceChildPosition) { + final View view = mViews.get(i); + if ((mReverseLayout && getPosition(view) <= referenceChildPosition) + || (!mReverseLayout && getPosition(view) >= referenceChildPosition)) { break; } if (view.hasFocusable()) { @@ -2987,8 +2831,9 @@ public View getFocusableViewAfter(int referenceChildPosition, int layoutDir) { } } else { for (int i = mViews.size() - 1; i >= 0; i--) { - View view = mViews.get(i); - if (mReverseLayout ? getPosition(view) >= referenceChildPosition : getPosition(view) <= referenceChildPosition) { + final View view = mViews.get(i); + if ((mReverseLayout && getPosition(view) >= referenceChildPosition) + || (!mReverseLayout && getPosition(view) <= referenceChildPosition)) { break; } if (view.hasFocusable()) { @@ -3002,6 +2847,244 @@ public View getFocusableViewAfter(int referenceChildPosition, int layoutDir) { } } + /** + * An array of mappings from adapter position to span. + * This only grows when a write happens and it grows up to the size of the adapter. + */ + static class LazySpanLookup { + + private static final int MIN_SIZE = 10; + int[] mData; + List mFullSpanItems; + + + /** + * Invalidates everything after this position, including full span information + */ + int forceInvalidateAfter(int position) { + if (mFullSpanItems != null) { + for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition >= position) { + mFullSpanItems.remove(i); + } + } + } + return invalidateAfter(position); + } + + /** + * returns end position for invalidation. + */ + int invalidateAfter(int position) { + if (mData == null) { + return RecyclerView.NO_POSITION; + } + if (position >= mData.length) { + return RecyclerView.NO_POSITION; + } + int endPosition = invalidateFullSpansAfter(position); + if (endPosition == RecyclerView.NO_POSITION) { + Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID); + return mData.length; + } else { + // Just invalidate items in between `position` and the next full span item, or the + // end of the tracked spans in mData if it's not been lengthened yet. + final int invalidateToIndex = Math.min(endPosition + 1, mData.length); + Arrays.fill(mData, position, invalidateToIndex, LayoutParams.INVALID_SPAN_ID); + return invalidateToIndex; + } + } + + int getSpan(int position) { + if (mData == null || position >= mData.length) { + return LayoutParams.INVALID_SPAN_ID; + } else { + return mData[position]; + } + } + + void setSpan(int position, Span span) { + ensureSize(position); + mData[position] = span.mIndex; + } + + int sizeForPosition(int position) { + int len = mData.length; + while (len <= position) { + len *= 2; + } + return len; + } + + void ensureSize(int position) { + if (mData == null) { + mData = new int[Math.max(position, MIN_SIZE) + 1]; + Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); + } else if (position >= mData.length) { + int[] old = mData; + mData = new int[sizeForPosition(position)]; + System.arraycopy(old, 0, mData, 0, old.length); + Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID); + } + } + + void clear() { + if (mData != null) { + Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); + } + mFullSpanItems = null; + } + + void offsetForRemoval(int positionStart, int itemCount) { + if (mData == null || positionStart >= mData.length) { + return; + } + ensureSize(positionStart + itemCount); + System.arraycopy(mData, positionStart + itemCount, mData, positionStart, + mData.length - positionStart - itemCount); + Arrays.fill(mData, mData.length - itemCount, mData.length, + LayoutParams.INVALID_SPAN_ID); + offsetFullSpansForRemoval(positionStart, itemCount); + } + + private void offsetFullSpansForRemoval(int positionStart, int itemCount) { + if (mFullSpanItems == null) { + return; + } + final int end = positionStart + itemCount; + for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition < positionStart) { + continue; + } + if (fsi.mPosition < end) { + mFullSpanItems.remove(i); + } else { + fsi.mPosition -= itemCount; + } + } + } + + void offsetForAddition(int positionStart, int itemCount) { + if (mData == null || positionStart >= mData.length) { + return; + } + ensureSize(positionStart + itemCount); + System.arraycopy(mData, positionStart, mData, positionStart + itemCount, + mData.length - positionStart - itemCount); + Arrays.fill(mData, positionStart, positionStart + itemCount, + LayoutParams.INVALID_SPAN_ID); + offsetFullSpansForAddition(positionStart, itemCount); + } + + private void offsetFullSpansForAddition(int positionStart, int itemCount) { + if (mFullSpanItems == null) { + return; + } + for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition < positionStart) { + continue; + } + fsi.mPosition += itemCount; + } + } + + /** + * Returns when invalidation should end. e.g. hitting a full span position. + * Returned position SHOULD BE invalidated. + */ + private int invalidateFullSpansAfter(int position) { + if (mFullSpanItems == null) { + return RecyclerView.NO_POSITION; + } + final FullSpanItem item = getFullSpanItem(position); + // if there is an fsi at this position, get rid of it. + if (item != null) { + mFullSpanItems.remove(item); + } + int nextFsiIndex = -1; + final int count = mFullSpanItems.size(); + for (int i = 0; i < count; i++) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition >= position) { + nextFsiIndex = i; + break; + } + } + if (nextFsiIndex != -1) { + FullSpanItem fsi = mFullSpanItems.get(nextFsiIndex); + mFullSpanItems.remove(nextFsiIndex); + return fsi.mPosition; + } + return RecyclerView.NO_POSITION; + } + + public void addFullSpanItem(FullSpanItem fullSpanItem) { + if (mFullSpanItems == null) { + mFullSpanItems = new ArrayList<>(); + } + final int size = mFullSpanItems.size(); + for (int i = 0; i < size; i++) { + FullSpanItem other = mFullSpanItems.get(i); + if (other.mPosition == fullSpanItem.mPosition) { + if (DEBUG) { + throw new IllegalStateException("two fsis for same position"); + } else { + mFullSpanItems.remove(i); + } + } + if (other.mPosition >= fullSpanItem.mPosition) { + mFullSpanItems.add(i, fullSpanItem); + return; + } + } + // if it is not added to a position. + mFullSpanItems.add(fullSpanItem); + } + + public FullSpanItem getFullSpanItem(int position) { + if (mFullSpanItems == null) { + return null; + } + for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { + final FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition == position) { + return fsi; + } + } + return null; + } + + /** + * @param minPos inclusive + * @param maxPos exclusive + * @param gapDir if not 0, returns FSIs on in that direction + * @param hasUnwantedGapAfter If true, when full span item has unwanted gaps, it will be + * returned even if its gap direction does not match. + */ + public FullSpanItem getFirstFullSpanItemInRange(int minPos, int maxPos, int gapDir, + boolean hasUnwantedGapAfter) { + if (mFullSpanItems == null) { + return null; + } + final int limit = mFullSpanItems.size(); + for (int i = 0; i < limit; i++) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition >= maxPos) { + return null; + } + if (fsi.mPosition >= minPos + && (gapDir == 0 || fsi.mGapDir == gapDir + || (hasUnwantedGapAfter && fsi.mHasUnwantedGapAfter))) { + return fsi; + } + } + return null; + } + } + /** * Data class to hold the information about an anchor position which is used in onLayout call. */ diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/ThreadUtil.java b/viewpager2/src/main/java/androidx/recyclerview/widget/ThreadUtil.java index d3438087d..541f38874 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/ThreadUtil.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/ThreadUtil.java @@ -20,16 +20,11 @@ interface ThreadUtil { - MainThreadCallback getMainThreadProxy(MainThreadCallback callback); - - BackgroundCallback getBackgroundProxy(BackgroundCallback callback); - interface MainThreadCallback { void updateItemCount(int generation, int itemCount); - @SuppressLint("UnknownNullness") - // b/240775049: Cannot annotate properly + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly void addTile(int generation, TileList.Tile tile); void removeTile(int generation, int position); @@ -44,8 +39,11 @@ void updateRange(int rangeStart, int rangeEnd, int extRangeStart, int extRangeEn void loadTile(int position, int scrollHint); - @SuppressLint("UnknownNullness") - // b/240775049: Cannot annotate properly + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly void recycleTile(TileList.Tile tile); } -} \ No newline at end of file + + MainThreadCallback getMainThreadProxy(MainThreadCallback callback); + + BackgroundCallback getBackgroundProxy(BackgroundCallback callback); +} diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/TileList.java b/viewpager2/src/main/java/androidx/recyclerview/widget/TileList.java index 718730189..ff7784f7f 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/TileList.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/TileList.java @@ -40,8 +40,8 @@ public TileList(int tileSize) { public T getItemAt(int pos) { if (mLastAccessedTile == null || !mLastAccessedTile.containsPosition(pos)) { - int startPosition = pos - (pos % mTileSize); - int index = mTiles.indexOfKey(startPosition); + final int startPosition = pos - (pos % mTileSize); + final int index = mTiles.indexOfKey(startPosition); if (index < 0) { return null; } @@ -70,7 +70,7 @@ public Tile getAtIndex(int index) { } public Tile addOrReplace(Tile newTile) { - int index = mTiles.indexOfKey(newTile.mStartPosition); + final int index = mTiles.indexOfKey(newTile.mStartPosition); if (index < 0) { mTiles.put(newTile.mStartPosition, newTile); return null; diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/ViewBoundsCheck.java b/viewpager2/src/main/java/androidx/recyclerview/widget/ViewBoundsCheck.java index 950a4e495..8ea8f7533 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/ViewBoundsCheck.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/ViewBoundsCheck.java @@ -29,7 +29,7 @@ */ class ViewBoundsCheck { - static final int GT = 1; + static final int GT = 1 << 0; static final int EQ = 1 << 1; static final int LT = 1 << 2; @@ -108,80 +108,7 @@ class ViewBoundsCheck { static final int MASK = GT | EQ | LT; final Callback mCallback; - final BoundFlags mBoundFlags; - - ViewBoundsCheck(Callback callback) { - mCallback = callback; - mBoundFlags = new BoundFlags(); - } - - /** - * Returns the first view starting from fromIndex to toIndex in views whose bounds lie within - * its parent bounds based on the provided preferredBoundFlags. If no match is found based on - * the preferred flags, and a nonzero acceptableBoundFlags is specified, the last view whose - * bounds lie within its parent view based on the acceptableBoundFlags is returned. If no such - * view is found based on either of these two flags, null is returned. - * - * @param fromIndex The view position index to start the search from. - * @param toIndex The view position index to end the search at. - * @param preferredBoundFlags The flags indicating the preferred match. Once a match is found - * based on this flag, that view is returned instantly. - * @param acceptableBoundFlags The flags indicating the acceptable match if no preferred match - * is found. If so, and if acceptableBoundFlags is non-zero, the - * last matching acceptable view is returned. Otherwise, null is - * returned. - * @return The first view that satisfies acceptableBoundFlags or the last view satisfying - * acceptableBoundFlags boundary conditions. - */ - View findOneViewWithinBoundFlags(int fromIndex, int toIndex, - @ViewBounds int preferredBoundFlags, - @ViewBounds int acceptableBoundFlags) { - int start = mCallback.getParentStart(); - int end = mCallback.getParentEnd(); - int next = toIndex > fromIndex ? 1 : -1; - View acceptableMatch = null; - for (int i = fromIndex; i != toIndex; i += next) { - View child = mCallback.getChildAt(i); - int childStart = mCallback.getChildStart(child); - int childEnd = mCallback.getChildEnd(child); - mBoundFlags.setBounds(start, end, childStart, childEnd); - if (preferredBoundFlags != 0) { - mBoundFlags.resetFlags(); - mBoundFlags.addFlags(preferredBoundFlags); - if (mBoundFlags.boundsMatch()) { - // found a perfect match - return child; - } - } - if (acceptableBoundFlags != 0) { - mBoundFlags.resetFlags(); - mBoundFlags.addFlags(acceptableBoundFlags); - if (mBoundFlags.boundsMatch()) { - acceptableMatch = child; - } - } - } - return acceptableMatch; - } - - /** - * Returns whether the specified view lies within the boundary condition of its parent view. - * - * @param child The child view to be checked. - * @param boundsFlags The flag against which the child view and parent view are matched. - * @return True if the view meets the boundsFlag, false otherwise. - */ - boolean isViewWithinBoundFlags(View child, @ViewBounds int boundsFlags) { - mBoundFlags.setBounds(mCallback.getParentStart(), mCallback.getParentEnd(), - mCallback.getChildStart(child), mCallback.getChildEnd(child)); - if (boundsFlags != 0) { - mBoundFlags.resetFlags(); - mBoundFlags.addFlags(boundsFlags); - return mBoundFlags.boundsMatch(); - } - return false; - } - + BoundFlags mBoundFlags; /** * The set of flags that can be passed for checking the view boundary conditions. * CVS in the flag name indicates the child view, and PV indicates the parent view.\ @@ -199,27 +126,15 @@ boolean isViewWithinBoundFlags(View child, @ViewBounds int boundsFlags) { FLAG_CVE_GT_PVE, FLAG_CVE_EQ_PVE, FLAG_CVE_LT_PVE }) @Retention(RetentionPolicy.SOURCE) - public @interface ViewBounds { - } + public @interface ViewBounds {} - /** - * Callback provided by the user of this class in order to retrieve information about child and - * parent boundaries. - */ - interface Callback { - View getChildAt(int index); - - int getParentStart(); - - int getParentEnd(); - - int getChildStart(View view); - - int getChildEnd(View view); + ViewBoundsCheck(Callback callback) { + mCallback = callback; + mBoundFlags = new BoundFlags(); } static class BoundFlags { - int mBoundFlags; + int mBoundFlags = 0; int mRvStart, mRvEnd, mChildStart, mChildEnd; void setBounds(int rvStart, int rvEnd, int childStart, int childEnd) { @@ -267,9 +182,88 @@ boolean boundsMatch() { } if ((mBoundFlags & (MASK << CVE_PVE_POS)) != 0) { - return (mBoundFlags & (compare(mChildEnd, mRvEnd) << CVE_PVE_POS)) != 0; + if ((mBoundFlags & (compare(mChildEnd, mRvEnd) << CVE_PVE_POS)) == 0) { + return false; + } } return true; } + }; + + /** + * Returns the first view starting from fromIndex to toIndex in views whose bounds lie within + * its parent bounds based on the provided preferredBoundFlags. If no match is found based on + * the preferred flags, and a nonzero acceptableBoundFlags is specified, the last view whose + * bounds lie within its parent view based on the acceptableBoundFlags is returned. If no such + * view is found based on either of these two flags, null is returned. + * @param fromIndex The view position index to start the search from. + * @param toIndex The view position index to end the search at. + * @param preferredBoundFlags The flags indicating the preferred match. Once a match is found + * based on this flag, that view is returned instantly. + * @param acceptableBoundFlags The flags indicating the acceptable match if no preferred match + * is found. If so, and if acceptableBoundFlags is non-zero, the + * last matching acceptable view is returned. Otherwise, null is + * returned. + * @return The first view that satisfies acceptableBoundFlags or the last view satisfying + * acceptableBoundFlags boundary conditions. + */ + View findOneViewWithinBoundFlags(int fromIndex, int toIndex, + @ViewBounds int preferredBoundFlags, + @ViewBounds int acceptableBoundFlags) { + final int start = mCallback.getParentStart(); + final int end = mCallback.getParentEnd(); + final int next = toIndex > fromIndex ? 1 : -1; + View acceptableMatch = null; + for (int i = fromIndex; i != toIndex; i += next) { + final View child = mCallback.getChildAt(i); + final int childStart = mCallback.getChildStart(child); + final int childEnd = mCallback.getChildEnd(child); + mBoundFlags.setBounds(start, end, childStart, childEnd); + if (preferredBoundFlags != 0) { + mBoundFlags.resetFlags(); + mBoundFlags.addFlags(preferredBoundFlags); + if (mBoundFlags.boundsMatch()) { + // found a perfect match + return child; + } + } + if (acceptableBoundFlags != 0) { + mBoundFlags.resetFlags(); + mBoundFlags.addFlags(acceptableBoundFlags); + if (mBoundFlags.boundsMatch()) { + acceptableMatch = child; + } + } + } + return acceptableMatch; + } + + /** + * Returns whether the specified view lies within the boundary condition of its parent view. + * @param child The child view to be checked. + * @param boundsFlags The flag against which the child view and parent view are matched. + * @return True if the view meets the boundsFlag, false otherwise. + */ + boolean isViewWithinBoundFlags(View child, @ViewBounds int boundsFlags) { + mBoundFlags.setBounds(mCallback.getParentStart(), mCallback.getParentEnd(), + mCallback.getChildStart(child), mCallback.getChildEnd(child)); + if (boundsFlags != 0) { + mBoundFlags.resetFlags(); + mBoundFlags.addFlags(boundsFlags); + return mBoundFlags.boundsMatch(); + } + return false; + } + + /** + * Callback provided by the user of this class in order to retrieve information about child and + * parent boundaries. + */ + interface Callback { + View getChildAt(int index); + int getParentStart(); + int getParentEnd(); + int getChildStart(View view); + int getChildEnd(View view); } } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/ViewInfoStore.java b/viewpager2/src/main/java/androidx/recyclerview/widget/ViewInfoStore.java index adb4c9c35..312edad35 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/ViewInfoStore.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/ViewInfoStore.java @@ -57,9 +57,8 @@ void clear() { /** * Adds the item information to the prelayout tracking - * * @param holder The ViewHolder whose information is being saved - * @param info The information to save + * @param info The information to save */ void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { InfoRecord record = mLayoutHolderMap.get(holder); @@ -72,7 +71,7 @@ record = InfoRecord.obtain(); } boolean isDisappearing(RecyclerView.ViewHolder holder) { - InfoRecord record = mLayoutHolderMap.get(holder); + final InfoRecord record = mLayoutHolderMap.get(holder); return record != null && ((record.flags & FLAG_DISAPPEARED) != 0); } @@ -103,10 +102,10 @@ private RecyclerView.ItemAnimator.ItemHolderInfo popFromLayoutStep(RecyclerView. if (index < 0) { return null; } - InfoRecord record = mLayoutHolderMap.valueAt(index); + final InfoRecord record = mLayoutHolderMap.valueAt(index); if (record != null && (record.flags & flag) != 0) { record.flags &= ~flag; - RecyclerView.ItemAnimator.ItemHolderInfo info; + final RecyclerView.ItemAnimator.ItemHolderInfo info; if (flag == FLAG_PRE) { info = record.preInfo; } else if (flag == FLAG_POST) { @@ -126,8 +125,7 @@ private RecyclerView.ItemAnimator.ItemHolderInfo popFromLayoutStep(RecyclerView. /** * Adds the given ViewHolder to the oldChangeHolders list - * - * @param key The key to identify the ViewHolder. + * @param key The key to identify the ViewHolder. * @param holder The ViewHolder to store */ void addToOldChangeHolders(long key, RecyclerView.ViewHolder holder) { @@ -141,7 +139,7 @@ void addToOldChangeHolders(long key, RecyclerView.ViewHolder holder) { * them. * * @param holder The ViewHolder to store - * @param info The information to save + * @param info The information to save */ void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { InfoRecord record = mLayoutHolderMap.get(holder); @@ -155,20 +153,20 @@ record = InfoRecord.obtain(); /** * Checks whether the given ViewHolder is in preLayout list - * * @param viewHolder The ViewHolder to query + * * @return True if the ViewHolder is present in preLayout, false otherwise */ boolean isInPreLayout(RecyclerView.ViewHolder viewHolder) { - InfoRecord record = mLayoutHolderMap.get(viewHolder); + final InfoRecord record = mLayoutHolderMap.get(viewHolder); return record != null && (record.flags & FLAG_PRE) != 0; } /** * Queries the oldChangeHolder list for the given key. If they are not tracked, simply returns * null. - * * @param key The key to be used to find the ViewHolder. + * * @return A ViewHolder if exists or null if it does not exist. */ RecyclerView.ViewHolder getFromOldChangeHolders(long key) { @@ -177,9 +175,8 @@ RecyclerView.ViewHolder getFromOldChangeHolders(long key) { /** * Adds the item information to the post layout list - * * @param holder The ViewHolder whose information is being saved - * @param info The information to save + * @param info The information to save */ void addToPostLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { InfoRecord record = mLayoutHolderMap.get(holder); @@ -208,7 +205,6 @@ record = InfoRecord.obtain(); /** * Removes a ViewHolder from disappearing list. - * * @param holder The ViewHolder to be removed from the disappearing list. */ void removeFromDisappearedInLayout(RecyclerView.ViewHolder holder) { @@ -221,8 +217,8 @@ void removeFromDisappearedInLayout(RecyclerView.ViewHolder holder) { void process(ProcessCallback callback) { for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) { - RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index); - InfoRecord record = mLayoutHolderMap.removeAt(index); + final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index); + final InfoRecord record = mLayoutHolderMap.removeAt(index); if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) { // Appeared then disappeared. Not useful for animations. callback.unused(viewHolder); @@ -258,7 +254,6 @@ void process(ProcessCallback callback) { /** * Removes the ViewHolder from all list - * * @param holder The ViewHolder which we should stop tracking */ void removeViewHolder(RecyclerView.ViewHolder holder) { @@ -268,7 +263,7 @@ void removeViewHolder(RecyclerView.ViewHolder holder) { break; } } - InfoRecord info = mLayoutHolderMap.remove(holder); + final InfoRecord info = mLayoutHolderMap.remove(holder); if (info != null) { InfoRecord.recycle(info); } @@ -284,14 +279,11 @@ public void onViewDetached(RecyclerView.ViewHolder viewHolder) { interface ProcessCallback { void processDisappeared(RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ItemAnimator.ItemHolderInfo preInfo, - @Nullable RecyclerView.ItemAnimator.ItemHolderInfo postInfo); - + @Nullable RecyclerView.ItemAnimator.ItemHolderInfo postInfo); void processAppeared(RecyclerView.ViewHolder viewHolder, @Nullable RecyclerView.ItemAnimator.ItemHolderInfo preInfo, - RecyclerView.ItemAnimator.ItemHolderInfo postInfo); - + RecyclerView.ItemAnimator.ItemHolderInfo postInfo); void processPersistent(RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ItemAnimator.ItemHolderInfo preInfo, - @NonNull RecyclerView.ItemAnimator.ItemHolderInfo postInfo); - + @NonNull RecyclerView.ItemAnimator.ItemHolderInfo postInfo); void unused(RecyclerView.ViewHolder holder); } @@ -307,12 +299,12 @@ static class InfoRecord { static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED; static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST; static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST; - static final Pools.Pool sPool = new Pools.SimplePool<>(20); int flags; @Nullable RecyclerView.ItemAnimator.ItemHolderInfo preInfo; @Nullable RecyclerView.ItemAnimator.ItemHolderInfo postInfo; + static Pools.Pool sPool = new Pools.SimplePool<>(20); private InfoRecord() { } @@ -331,7 +323,7 @@ static void recycle(InfoRecord record) { static void drainCache() { //noinspection StatementWithEmptyBody - while (sPool.acquire() != null) ; + while (sPool.acquire() != null); } } } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/ViewTypeStorage.java b/viewpager2/src/main/java/androidx/recyclerview/widget/ViewTypeStorage.java index 6f4fea0e3..adf9d01d7 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/ViewTypeStorage.java +++ b/viewpager2/src/main/java/androidx/recyclerview/widget/ViewTypeStorage.java @@ -50,7 +50,7 @@ interface ViewTypeLookup { class SharedIdRangeViewTypeStorage implements ViewTypeStorage { // we keep a list of nested wrappers here even though we only need 1 to create because // they might be removed. - final SparseArray> mGlobalTypeToWrapper = new SparseArray<>(); + SparseArray> mGlobalTypeToWrapper = new SparseArray<>(); @NonNull @Override @@ -118,9 +118,9 @@ public void dispose() { } class IsolatedViewTypeStorage implements ViewTypeStorage { - final SparseArray mGlobalTypeToWrapper = new SparseArray<>(); + SparseArray mGlobalTypeToWrapper = new SparseArray<>(); - int mNextViewType; + int mNextViewType = 0; int obtainViewType(NestedAdapterWrapper wrapper) { int nextId = mNextViewType++; @@ -157,9 +157,9 @@ void removeWrapper(@NonNull NestedAdapterWrapper wrapper) { } class WrapperViewTypeLookup implements ViewTypeLookup { + private SparseIntArray mLocalToGlobalMapping = new SparseIntArray(1); + private SparseIntArray mGlobalToLocalMapping = new SparseIntArray(1); final NestedAdapterWrapper mWrapper; - private final SparseIntArray mLocalToGlobalMapping = new SparseIntArray(1); - private final SparseIntArray mGlobalToLocalMapping = new SparseIntArray(1); WrapperViewTypeLookup(NestedAdapterWrapper wrapper) { mWrapper = wrapper; diff --git a/viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentStateAdapter.java b/viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentStateAdapter.java index ee3d33afe..0b8499eba 100644 --- a/viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentStateAdapter.java +++ b/viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentStateAdapter.java @@ -22,7 +22,6 @@ import static androidx.recyclerview.widget.RecyclerView.NO_ID; import static androidx.viewpager2.adapter.FragmentStateAdapter.FragmentTransactionCallback.OnPostEventListener; -import android.annotation.SuppressLint; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -63,7 +62,7 @@ * re-usable container for a {@link Fragment} in later stages. *

  • {@link RecyclerView.Adapter#onBindViewHolder} we ask for a {@link Fragment} for the * position. If we already have the fragment, or have previously saved its state, we use those. - *
  • we attach the {@link Fragment} to a + *
  • {@link RecyclerView.Adapter#onAttachedToWindow} we attach the {@link Fragment} to a * container. *
  • {@link RecyclerView.Adapter#onViewRecycled} we remove, save state, destroy the * {@link Fragment}. @@ -86,20 +85,23 @@ public abstract class FragmentStateAdapter extends // Fragment bookkeeping @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor final LongSparseArray mFragments = new LongSparseArray<>(); - @SuppressWarnings("WeakerAccess") - final // to avoid creation of a synthetic accessor - FragmentEventDispatcher mFragmentEventDispatcher = new FragmentEventDispatcher(); private final LongSparseArray mSavedStates = new LongSparseArray<>(); private final LongSparseArray mItemIdToViewHolder = new LongSparseArray<>(); + + private FragmentMaxLifecycleEnforcer mFragmentMaxLifecycleEnforcer; + + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor + FragmentEventDispatcher mFragmentEventDispatcher = new FragmentEventDispatcher(); + // Fragment GC @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor - boolean mIsInGracePeriod; - private FragmentMaxLifecycleEnforcer mFragmentMaxLifecycleEnforcer; - private boolean mHasStaleFragments; + boolean mIsInGracePeriod = false; + private boolean mHasStaleFragments = false; /** * @param fragmentActivity if the {@link ViewPager2} lives directly in a - * {@link FragmentActivity} subclass. + * {@link FragmentActivity} subclass. + * * @see FragmentStateAdapter#FragmentStateAdapter(Fragment) * @see FragmentStateAdapter#FragmentStateAdapter(FragmentManager, Lifecycle) */ @@ -109,6 +111,7 @@ public FragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) { /** * @param fragment if the {@link ViewPager2} lives directly in a {@link Fragment} subclass. + * * @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity) * @see FragmentStateAdapter#FragmentStateAdapter(FragmentManager, Lifecycle) */ @@ -118,34 +121,18 @@ public FragmentStateAdapter(@NonNull Fragment fragment) { /** * @param fragmentManager of {@link ViewPager2}'s host - * @param lifecycle of {@link ViewPager2}'s host + * @param lifecycle of {@link ViewPager2}'s host + * * @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity) * @see FragmentStateAdapter#FragmentStateAdapter(Fragment) */ public FragmentStateAdapter(@NonNull FragmentManager fragmentManager, - @NonNull Lifecycle lifecycle) { + @NonNull Lifecycle lifecycle) { mFragmentManager = fragmentManager; mLifecycle = lifecycle; super.setHasStableIds(true); } - // Helper function for dealing with save / restore state - private static @NonNull - String createKey(@NonNull String prefix, long id) { - return prefix + id; - } - - // Helper function for dealing with save / restore state - private static boolean isValidKey(@NonNull String key, @NonNull String prefix) { - return key.startsWith(prefix) && key.length() > prefix.length(); - } - - // Helper function for dealing with save / restore state - private static long parseIdFromKey(@NonNull String key, @NonNull String prefix) { - return Long.parseLong(key.substring(prefix.length())); - } - - @SuppressLint("RestrictedApi") @CallSuper @Override public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { @@ -171,11 +158,9 @@ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { * will be saved. When the item is close to the viewport again, a new Fragment will be * requested, and a previously saved state will be used to initialize it. * - * * @see ViewPager2#setOffscreenPageLimit */ - public abstract @NonNull - Fragment createFragment(int position); + public abstract @NonNull Fragment createFragment(int position); @NonNull @Override @@ -184,10 +169,10 @@ public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, in } @Override - public final void onBindViewHolder(@NonNull FragmentViewHolder holder, int position) { - long itemId = holder.getItemId(); - int viewHolderId = holder.getContainer().getId(); - Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH + public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) { + final long itemId = holder.getItemId(); + final int viewHolderId = holder.getContainer().getId(); + final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH if (boundItemId != null && boundItemId != itemId) { removeFragment(boundItemId); mItemIdToViewHolder.remove(boundItemId); @@ -198,7 +183,7 @@ public final void onBindViewHolder(@NonNull FragmentViewHolder holder, int posit /** Special case when {@link RecyclerView} decides to keep the {@link container} * attached to the window, but not to the view hierarchy (i.e. parent is null) */ - FrameLayout container = holder.getContainer(); + final FrameLayout container = holder.getContainer(); if (ViewCompat.isAttachedToWindow(container)) { if (container.getParent() != null) { throw new IllegalStateException("Design assumption violated."); @@ -206,7 +191,7 @@ public final void onBindViewHolder(@NonNull FragmentViewHolder holder, int posit container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { + int oldLeft, int oldTop, int oldRight, int oldBottom) { if (container.getParent() != null) { container.removeOnLayoutChangeListener(this); placeFragmentInViewHolder(holder); @@ -218,20 +203,7 @@ public void onLayoutChange(View v, int left, int top, int right, int bottom, gcFragments(); } - public void clearAllFragments() { - Set toRemove = new ArraySet<>(); - for (int ix = 0; ix < mFragments.size(); ix++) { - long itemId = mFragments.keyAt(ix); - mItemIdToViewHolder.remove(itemId); - toRemove.add(itemId); - } - for (Long itemId : toRemove) { - removeFragment(itemId); - } - } - - @SuppressWarnings("WeakerAccess") - // to avoid creation of a synthetic accessor + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor void gcFragments() { if (!mHasStaleFragments || shouldDelayFragmentTransactions()) { return; @@ -316,7 +288,7 @@ public final Fragment findByPosition(int position) { } @Override - public final void onViewAttachedToWindow(@NonNull FragmentViewHolder holder) { + public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) { placeFragmentInViewHolder(holder); gcFragments(); } @@ -324,9 +296,8 @@ public final void onViewAttachedToWindow(@NonNull FragmentViewHolder holder) { /** * @param holder that has been bound to a Fragment in the {@link #onBindViewHolder} stage. */ - @SuppressWarnings("WeakerAccess") - // to avoid creation of a synthetic accessor - void placeFragmentInViewHolder(@NonNull FragmentViewHolder holder) { + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor + void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) { Fragment fragment = mFragments.get(holder.getItemId()); if (fragment == null) { throw new IllegalStateException("Design assumption violated."); @@ -399,7 +370,7 @@ void placeFragmentInViewHolder(@NonNull FragmentViewHolder holder) { mLifecycle.addObserver(new LifecycleEventObserver() { @Override public void onStateChanged(@NonNull LifecycleOwner source, - @NonNull Lifecycle.Event event) { + @NonNull Lifecycle.Event event) { if (shouldDelayFragmentTransactions()) { return; } @@ -412,7 +383,7 @@ public void onStateChanged(@NonNull LifecycleOwner source, } } - private void scheduleViewAttach(Fragment fragment, @NonNull FrameLayout container) { + private void scheduleViewAttach(final Fragment fragment, @NonNull final FrameLayout container) { // After a config change, Fragments that were in FragmentManager will be recreated. Since // ViewHolder container ids are dynamically generated, we opted to manually handle // attaching Fragment views to containers. For consistency, we use the same mechanism for @@ -423,8 +394,8 @@ private void scheduleViewAttach(Fragment fragment, @NonNull FrameLayout containe @SuppressWarnings("ReferenceEquality") @Override public void onFragmentViewCreated(@NonNull FragmentManager fm, - @NonNull Fragment f, @NonNull View v, - @Nullable Bundle savedInstanceState) { + @NonNull Fragment f, @NonNull View v, + @Nullable Bundle savedInstanceState) { if (f == fragment) { fm.unregisterFragmentLifecycleCallbacks(this); addViewToContainer(v, container); @@ -433,8 +404,7 @@ public void onFragmentViewCreated(@NonNull FragmentManager fm, }, false); } - @SuppressWarnings("WeakerAccess") - // to avoid creation of a synthetic accessor + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor void addViewToContainer(@NonNull View v, @NonNull FrameLayout container) { if (container.getChildCount() > 1) { throw new IllegalStateException("Design assumption violated."); @@ -457,8 +427,8 @@ void addViewToContainer(@NonNull View v, @NonNull FrameLayout container) { @Override public final void onViewRecycled(@NonNull FragmentViewHolder holder) { - int viewHolderId = holder.getContainer().getId(); - Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH + final int viewHolderId = holder.getContainer().getId(); + final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH if (boundItemId != null) { removeFragment(boundItemId); mItemIdToViewHolder.remove(boundItemId); @@ -529,8 +499,7 @@ private void removeFragment(long itemId) { } } - @SuppressWarnings("WeakerAccess") - // to avoid creation of a synthetic accessor + @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor boolean shouldDelayFragmentTransactions() { return mFragmentManager.isStateSaved(); } @@ -569,8 +538,7 @@ public final void setHasStableIds(boolean hasStableIds) { } @Override - public final @NonNull - Parcelable saveState() { + public final @NonNull Parcelable saveState() { /** TODO(b/122670461): use custom {@link Parcelable} instead of Bundle to save space */ Bundle savedState = new Bundle(mFragments.size() + mSavedStates.size()); @@ -596,8 +564,8 @@ Parcelable saveState() { return savedState; } - @SuppressWarnings("deprecation") @Override + @SuppressWarnings("deprecation") public final void restoreState(@NonNull Parcelable savedState) { if (!mSavedStates.isEmpty() || !mFragments.isEmpty()) { throw new IllegalStateException( @@ -644,8 +612,8 @@ public final void restoreState(@NonNull Parcelable savedState) { } private void scheduleGracePeriodEnd() { - Handler handler = new Handler(Looper.getMainLooper()); - Runnable runnable = () -> { + final Handler handler = new Handler(Looper.getMainLooper()); + final Runnable runnable = () -> { mIsInGracePeriod = false; gcFragments(); // good opportunity to GC }; @@ -653,7 +621,7 @@ private void scheduleGracePeriodEnd() { mLifecycle.addObserver(new LifecycleEventObserver() { @Override public void onStateChanged(@NonNull LifecycleOwner source, - @NonNull Lifecycle.Event event) { + @NonNull Lifecycle.Event event) { if (event == Lifecycle.Event.ON_DESTROY) { handler.removeCallbacks(runnable); source.getLifecycle().removeObserver(this); @@ -664,25 +632,148 @@ public void onStateChanged(@NonNull LifecycleOwner source, handler.postDelayed(runnable, GRACE_WINDOW_TIME_MS); } - /** - * Registers a {@link FragmentTransactionCallback} to listen to fragment lifecycle changes - * that happen inside the adapter. - * - * @param callback Callback to register - */ - public void registerFragmentTransactionCallback(@NonNull FragmentTransactionCallback callback) { - mFragmentEventDispatcher.registerCallback(callback); + // Helper function for dealing with save / restore state + private static @NonNull String createKey(@NonNull String prefix, long id) { + return prefix + id; + } + + // Helper function for dealing with save / restore state + private static boolean isValidKey(@NonNull String key, @NonNull String prefix) { + return key.startsWith(prefix) && key.length() > prefix.length(); + } + + // Helper function for dealing with save / restore state + private static long parseIdFromKey(@NonNull String key, @NonNull String prefix) { + return Long.parseLong(key.substring(prefix.length())); } /** - * Unregisters a {@link FragmentTransactionCallback}. - * - * @param callback Callback to unregister - * @see #registerFragmentTransactionCallback + * Pauses (STARTED) all Fragments that are attached and not a primary item. + * Keeps primary item Fragment RESUMED. */ - public void unregisterFragmentTransactionCallback( - @NonNull FragmentTransactionCallback callback) { - mFragmentEventDispatcher.unregisterCallback(callback); + class FragmentMaxLifecycleEnforcer { + private ViewPager2.OnPageChangeCallback mPageChangeCallback; + private RecyclerView.AdapterDataObserver mDataObserver; + private LifecycleEventObserver mLifecycleObserver; + private ViewPager2 mViewPager; + + private long mPrimaryItemId = NO_ID; + + void register(@NonNull RecyclerView recyclerView) { + mViewPager = inferViewPager(recyclerView); + + // signal 1 of 3: current item has changed + mPageChangeCallback = new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageScrollStateChanged(int state) { + updateFragmentMaxLifecycle(false); + } + + @Override + public void onPageSelected(int position) { + updateFragmentMaxLifecycle(false); + } + }; + mViewPager.registerOnPageChangeCallback(mPageChangeCallback); + + // signal 2 of 3: underlying data-set has been updated + mDataObserver = new DataSetChangeObserver() { + @Override + public void onChanged() { + updateFragmentMaxLifecycle(true); + } + }; + registerAdapterDataObserver(mDataObserver); + + // signal 3 of 3: we may have to catch-up after being in a lifecycle state that + // prevented us to perform transactions + mLifecycleObserver = (source, event) -> updateFragmentMaxLifecycle(false); + mLifecycle.addObserver(mLifecycleObserver); + } + + void unregister(@NonNull RecyclerView recyclerView) { + ViewPager2 viewPager = inferViewPager(recyclerView); + viewPager.unregisterOnPageChangeCallback(mPageChangeCallback); + unregisterAdapterDataObserver(mDataObserver); + mLifecycle.removeObserver(mLifecycleObserver); + mViewPager = null; + } + + void updateFragmentMaxLifecycle(boolean dataSetChanged) { + if (shouldDelayFragmentTransactions()) { + return; /** recovery step via {@link #mLifecycleObserver} */ + } + + if (mViewPager.getScrollState() != ViewPager2.SCROLL_STATE_IDLE) { + return; // do not update while not idle to avoid jitter + } + + if (mFragments.isEmpty() || getItemCount() == 0) { + return; // nothing to do + } + + final int currentItem = mViewPager.getCurrentItem(); + if (currentItem >= getItemCount()) { + /** current item is yet to be updated; it is guaranteed to change, so we will be + * notified via {@link ViewPager2.OnPageChangeCallback#onPageSelected(int)} */ + return; + } + + long currentItemId = getItemId(currentItem); + if (currentItemId == mPrimaryItemId && !dataSetChanged) { + return; // nothing to do + } + + Fragment currentItemFragment = mFragments.get(currentItemId); + if (currentItemFragment == null || !currentItemFragment.isAdded()) { + return; + } + + mPrimaryItemId = currentItemId; + FragmentTransaction transaction = mFragmentManager.beginTransaction(); + + Fragment toResume = null; + List> onPost = new ArrayList<>(); + for (int ix = 0; ix < mFragments.size(); ix++) { + long itemId = mFragments.keyAt(ix); + Fragment fragment = mFragments.valueAt(ix); + + if (!fragment.isAdded()) { + continue; + } + + if (itemId != mPrimaryItemId) { + transaction.setMaxLifecycle(fragment, STARTED); + onPost.add(mFragmentEventDispatcher.dispatchMaxLifecyclePreUpdated(fragment, + STARTED)); + } else { + toResume = fragment; // itemId map key, so only one can match the predicate + } + + fragment.setMenuVisibility(itemId == mPrimaryItemId); + } + if (toResume != null) { // in case the Fragment wasn't added yet + transaction.setMaxLifecycle(toResume, RESUMED); + onPost.add(mFragmentEventDispatcher.dispatchMaxLifecyclePreUpdated(toResume, + RESUMED)); + } + + if (!transaction.isEmpty()) { + transaction.commitNow(); + Collections.reverse(onPost); // to assure 'nesting' of events + for (List event : onPost) { + mFragmentEventDispatcher.dispatchPostEvents(event); + } + } + } + @NonNull + private ViewPager2 inferViewPager(@NonNull RecyclerView recyclerView) { + ViewParent parent = recyclerView.getParent(); + if (parent instanceof ViewPager2) { + return (ViewPager2) parent; + } + throw new IllegalStateException("Expected ViewPager2 instance. Got: " + parent); + } } /** @@ -700,7 +791,7 @@ public final void onItemRangeChanged(int positionStart, int itemCount) { @Override public final void onItemRangeChanged(int positionStart, int itemCount, - @Nullable Object payload) { + @Nullable Object payload) { onChanged(); } @@ -722,7 +813,7 @@ public final void onItemRangeMoved(int fromPosition, int toPosition, int itemCou @SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor static class FragmentEventDispatcher { - private final List mCallbacks = new CopyOnWriteArrayList<>(); + private List mCallbacks = new CopyOnWriteArrayList<>(); public void registerCallback(FragmentTransactionCallback callback) { mCallbacks.add(callback); @@ -733,7 +824,7 @@ public void unregisterCallback(FragmentTransactionCallback callback) { } public List dispatchMaxLifecyclePreUpdated(Fragment fragment, - Lifecycle.State maxState) { + Lifecycle.State maxState) { List result = new ArrayList<>(); for (FragmentTransactionCallback callback : mCallbacks) { result.add(callback.onFragmentMaxLifecyclePreUpdated(fragment, maxState)); @@ -777,8 +868,7 @@ public List dispatchPreRemoved(Fragment fragment) { * inside the adapter. */ public abstract static class FragmentTransactionCallback { - private static final @NonNull - OnPostEventListener NO_OP = () -> { + private static final @NonNull OnPostEventListener NO_OP = () -> { // do nothing }; @@ -820,13 +910,13 @@ public OnPostEventListener onFragmentPreRemoved(@NonNull Fragment fragment) { * Called right before Fragment's maximum state is capped via * {@link FragmentTransaction#setMaxLifecycle}. * - * @param fragment Fragment to have its state capped + * @param fragment Fragment to have its state capped * @param maxLifecycleState Ceiling state for the fragment * @return Listener called after the operation */ @NonNull public OnPostEventListener onFragmentMaxLifecyclePreUpdated(@NonNull Fragment fragment, - @NonNull Lifecycle.State maxLifecycleState) { + @NonNull Lifecycle.State maxLifecycleState) { return NO_OP; } @@ -835,140 +925,29 @@ public OnPostEventListener onFragmentMaxLifecyclePreUpdated(@NonNull Fragment fr * {@link #onFragmentMaxLifecyclePreUpdated} called after the operation ends. */ public interface OnPostEventListener { - /** - * Called after the operation is ends. - */ + /** Called after the operation is ends. */ void onPost(); } } /** - * Pauses (STARTED) all Fragments that are attached and not a primary item. - * Keeps primary item Fragment RESUMED. + * Registers a {@link FragmentTransactionCallback} to listen to fragment lifecycle changes + * that happen inside the adapter. + * + * @param callback Callback to register */ - class FragmentMaxLifecycleEnforcer { - private ViewPager2.OnPageChangeCallback mPageChangeCallback; - private RecyclerView.AdapterDataObserver mDataObserver; - private LifecycleEventObserver mLifecycleObserver; - private ViewPager2 mViewPager; - - private long mPrimaryItemId = NO_ID; - - void register(@NonNull RecyclerView recyclerView) { - mViewPager = inferViewPager(recyclerView); - - // signal 1 of 3: current item has changed - mPageChangeCallback = new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageScrollStateChanged(int state) { - updateFragmentMaxLifecycle(false); - } - - @Override - public void onPageSelected(int position) { - updateFragmentMaxLifecycle(false); - } - }; - mViewPager.registerOnPageChangeCallback(mPageChangeCallback); - - // signal 2 of 3: underlying data-set has been updated - mDataObserver = new DataSetChangeObserver() { - @Override - public void onChanged() { - updateFragmentMaxLifecycle(true); - } - }; - registerAdapterDataObserver(mDataObserver); - - // signal 3 of 3: we may have to catch-up after being in a lifecycle state that - // prevented us to perform transactions - mLifecycleObserver = (source, event) -> updateFragmentMaxLifecycle(false); - mLifecycle.addObserver(mLifecycleObserver); - } - - void unregister(@NonNull RecyclerView recyclerView) { - ViewPager2 viewPager = inferViewPager(recyclerView); - viewPager.unregisterOnPageChangeCallback(mPageChangeCallback); - unregisterAdapterDataObserver(mDataObserver); - mLifecycle.removeObserver(mLifecycleObserver); - mViewPager = null; - } - - void updateFragmentMaxLifecycle(boolean dataSetChanged) { - if (shouldDelayFragmentTransactions()) { - return; /** recovery step via {@link #mLifecycleObserver} */ - } - - if (mViewPager.getScrollState() != ViewPager2.SCROLL_STATE_IDLE) { - return; // do not update while not idle to avoid jitter - } - - if (mFragments.isEmpty() || getItemCount() == 0) { - return; // nothing to do - } - - int currentItem = mViewPager.getCurrentItem(); - if (currentItem >= getItemCount()) { - /** current item is yet to be updated; it is guaranteed to change, so we will be - * notified via {@link ViewPager2.OnPageChangeCallback#onPageSelected(int)} */ - return; - } - - long currentItemId = getItemId(currentItem); - if (currentItemId == mPrimaryItemId && !dataSetChanged) { - return; // nothing to do - } - - Fragment currentItemFragment = mFragments.get(currentItemId); - if (currentItemFragment == null || !currentItemFragment.isAdded()) { - return; - } - - mPrimaryItemId = currentItemId; - FragmentTransaction transaction = mFragmentManager.beginTransaction(); - - Fragment toResume = null; - List> onPost = new ArrayList<>(); - for (int ix = 0; ix < mFragments.size(); ix++) { - long itemId = mFragments.keyAt(ix); - Fragment fragment = mFragments.valueAt(ix); - - if (!fragment.isAdded()) { - continue; - } - - if (itemId != mPrimaryItemId) { - transaction.setMaxLifecycle(fragment, STARTED); - onPost.add(mFragmentEventDispatcher.dispatchMaxLifecyclePreUpdated(fragment, - STARTED)); - } else { - toResume = fragment; // itemId map key, so only one can match the predicate - } - - fragment.setMenuVisibility(itemId == mPrimaryItemId); - } - if (toResume != null) { // in case the Fragment wasn't added yet - transaction.setMaxLifecycle(toResume, RESUMED); - onPost.add(mFragmentEventDispatcher.dispatchMaxLifecyclePreUpdated(toResume, - RESUMED)); - } - - if (!transaction.isEmpty()) { - transaction.commitNow(); - Collections.reverse(onPost); // to assure 'nesting' of events - for (List event : onPost) { - mFragmentEventDispatcher.dispatchPostEvents(event); - } - } - } + public void registerFragmentTransactionCallback(@NonNull FragmentTransactionCallback callback) { + mFragmentEventDispatcher.registerCallback(callback); + } - @NonNull - private ViewPager2 inferViewPager(@NonNull RecyclerView recyclerView) { - ViewParent parent = recyclerView.getParent(); - if (parent instanceof ViewPager2) { - return (ViewPager2) parent; - } - throw new IllegalStateException("Expected ViewPager2 instance. Got: " + parent); - } + /** + * Unregisters a {@link FragmentTransactionCallback}. + * + * @param callback Callback to unregister + * @see #registerFragmentTransactionCallback + */ + public void unregisterFragmentTransactionCallback( + @NonNull FragmentTransactionCallback callback) { + mFragmentEventDispatcher.unregisterCallback(callback); } } diff --git a/viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentViewHolder.java b/viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentViewHolder.java index 54174f34b..bcb5b2d79 100644 --- a/viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentViewHolder.java +++ b/viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentViewHolder.java @@ -33,8 +33,7 @@ private FragmentViewHolder(@NonNull FrameLayout container) { super(container); } - @NonNull - static FragmentViewHolder create(@NonNull ViewGroup parent) { + @NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) { FrameLayout container = new FrameLayout(parent.getContext()); container.setLayoutParams( new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, @@ -44,8 +43,7 @@ static FragmentViewHolder create(@NonNull ViewGroup parent) { return new FragmentViewHolder(container); } - @NonNull - FrameLayout getContainer() { + @NonNull FrameLayout getContainer() { return (FrameLayout) itemView; } } diff --git a/viewpager2/src/main/java/androidx/viewpager2/adapter/StatefulAdapter.java b/viewpager2/src/main/java/androidx/viewpager2/adapter/StatefulAdapter.java index dae1d3b2d..02596441f 100644 --- a/viewpager2/src/main/java/androidx/viewpager2/adapter/StatefulAdapter.java +++ b/viewpager2/src/main/java/androidx/viewpager2/adapter/StatefulAdapter.java @@ -27,14 +27,9 @@ * {@link View#onSaveInstanceState()} and {@link View#onRestoreInstanceState(Parcelable)} */ public interface StatefulAdapter { - /** - * Saves adapter state - */ - @NonNull - Parcelable saveState(); + /** Saves adapter state */ + @NonNull Parcelable saveState(); - /** - * Restores adapter state - */ + /** Restores adapter state */ void restoreState(@NonNull Parcelable savedState); } diff --git a/viewpager2/src/main/java/androidx/viewpager2/widget/AnimateLayoutChangeDetector.java b/viewpager2/src/main/java/androidx/viewpager2/widget/AnimateLayoutChangeDetector.java index bddcf178f..0b5728b40 100644 --- a/viewpager2/src/main/java/androidx/viewpager2/widget/AnimateLayoutChangeDetector.java +++ b/viewpager2/src/main/java/androidx/viewpager2/widget/AnimateLayoutChangeDetector.java @@ -31,7 +31,7 @@ /** * Class used to detect if there are gaps between pages and if any of the pages contain a running * change-transition in case we detected an illegal state in the {@link ScrollEventAdapter}. - *

    + * * This is an approximation of the detection and could potentially lead to misleading advice. If we * hit problems with it, remove the detection and replace with a suggestive error message instead, * like "Negative page offset encountered. Did you setAnimateParentHierarchy(false) to all your @@ -45,29 +45,12 @@ final class AnimateLayoutChangeDetector { ZERO_MARGIN_LAYOUT_PARAMS.setMargins(0, 0, 0, 0); } - private final LinearLayoutManager mLayoutManager; + private LinearLayoutManager mLayoutManager; AnimateLayoutChangeDetector(@NonNull LinearLayoutManager llm) { mLayoutManager = llm; } - private static boolean hasRunningChangingLayoutTransition(View view) { - if (view instanceof ViewGroup) { - ViewGroup viewGroup = (ViewGroup) view; - LayoutTransition layoutTransition = viewGroup.getLayoutTransition(); - if (layoutTransition != null && layoutTransition.isChangingLayout()) { - return true; - } - int childCount = viewGroup.getChildCount(); - for (int i = 0; i < childCount; i++) { - if (hasRunningChangingLayoutTransition(viewGroup.getChildAt(i))) { - return true; - } - } - } - return false; - } - boolean mayHaveInterferingAnimations() { // Two conditions need to be satisfied: // 1) the pages are not laid out contiguously (i.e., there are gaps between them) @@ -117,7 +100,10 @@ private boolean arePagesLaidOutContiguously() { // Check that the pages fill the whole screen int pageSize = bounds[0][1] - bounds[0][0]; - return bounds[0][0] <= 0 && bounds[childCount - 1][1] >= pageSize; + if (bounds[0][0] > 0 || bounds[childCount - 1][1] < pageSize) { + return false; + } + return true; } private boolean hasRunningChangingLayoutTransition() { @@ -129,4 +115,21 @@ private boolean hasRunningChangingLayoutTransition() { } return false; } + + private static boolean hasRunningChangingLayoutTransition(View view) { + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + LayoutTransition layoutTransition = viewGroup.getLayoutTransition(); + if (layoutTransition != null && layoutTransition.isChangingLayout()) { + return true; + } + int childCount = viewGroup.getChildCount(); + for (int i = 0; i < childCount; i++) { + if (hasRunningChangingLayoutTransition(viewGroup.getChildAt(i))) { + return true; + } + } + } + return false; + } } diff --git a/viewpager2/src/main/java/androidx/viewpager2/widget/CompositePageTransformer.java b/viewpager2/src/main/java/androidx/viewpager2/widget/CompositePageTransformer.java index 5d0309b0a..714dae24e 100644 --- a/viewpager2/src/main/java/androidx/viewpager2/widget/CompositePageTransformer.java +++ b/viewpager2/src/main/java/androidx/viewpager2/widget/CompositePageTransformer.java @@ -42,9 +42,7 @@ public void addTransformer(@NonNull PageTransformer transformer) { mTransformers.add(transformer); } - /** - * Removes a page transformer from the list. - */ + /** Removes a page transformer from the list. */ public void removeTransformer(@NonNull PageTransformer transformer) { mTransformers.remove(transformer); } diff --git a/viewpager2/src/main/java/androidx/viewpager2/widget/FakeDrag.java b/viewpager2/src/main/java/androidx/viewpager2/widget/FakeDrag.java index 75bbaf129..ac9d3c4fc 100644 --- a/viewpager2/src/main/java/androidx/viewpager2/widget/FakeDrag.java +++ b/viewpager2/src/main/java/androidx/viewpager2/widget/FakeDrag.java @@ -41,7 +41,7 @@ final class FakeDrag { private long mFakeDragBeginTime; FakeDrag(ViewPager2 viewPager, ScrollEventAdapter scrollEventAdapter, - RecyclerView recyclerView) { + RecyclerView recyclerView) { mViewPager = viewPager; mScrollEventAdapter = scrollEventAdapter; mRecyclerView = recyclerView; @@ -86,11 +86,11 @@ boolean fakeDragBy(float offsetPxFloat) { boolean isHorizontal = mViewPager.getOrientation() == ORIENTATION_HORIZONTAL; // Scroll deltas use pixels: - int offsetX = isHorizontal ? offsetPx : 0; - int offsetY = isHorizontal ? 0 : offsetPx; + final int offsetX = isHorizontal ? offsetPx : 0; + final int offsetY = isHorizontal ? 0 : offsetPx; // Motion events get the raw float distance: - float x = isHorizontal ? mRequestedDragDistance : 0; - float y = isHorizontal ? 0 : mRequestedDragDistance; + final float x = isHorizontal ? mRequestedDragDistance : 0; + final float y = isHorizontal ? 0 : mRequestedDragDistance; mRecyclerView.scrollBy(offsetX, offsetY); addFakeMotionEvent(time, MotionEvent.ACTION_MOVE, x, y); @@ -108,7 +108,7 @@ boolean endFakeDrag() { // Compute the velocity of the fake drag final int pixelsPerSecond = 1000; - VelocityTracker velocityTracker = mVelocityTracker; + final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(pixelsPerSecond, mMaximumVelocity); int xVelocity = (int) velocityTracker.getXVelocity(); int yVelocity = (int) velocityTracker.getYVelocity(); @@ -123,7 +123,7 @@ boolean endFakeDrag() { private void beginFakeVelocityTracker() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); - ViewConfiguration configuration = ViewConfiguration.get(mViewPager.getContext()); + final ViewConfiguration configuration = ViewConfiguration.get(mViewPager.getContext()); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); } else { mVelocityTracker.clear(); @@ -131,7 +131,7 @@ private void beginFakeVelocityTracker() { } private void addFakeMotionEvent(long time, int action, float x, float y) { - MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, action, x, y, 0); + final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, action, x, y, 0); mVelocityTracker.addMovement(ev); ev.recycle(); } diff --git a/viewpager2/src/main/java/androidx/viewpager2/widget/ScrollEventAdapter.java b/viewpager2/src/main/java/androidx/viewpager2/widget/ScrollEventAdapter.java index 2c398d8ae..cd59b0edd 100644 --- a/viewpager2/src/main/java/androidx/viewpager2/widget/ScrollEventAdapter.java +++ b/viewpager2/src/main/java/androidx/viewpager2/widget/ScrollEventAdapter.java @@ -42,25 +42,30 @@ * relative to the pages and exposes this position via ({@link #getRelativeScrollPosition()}. */ final class ScrollEventAdapter extends RecyclerView.OnScrollListener { + /** @hide */ + @Retention(SOURCE) + @IntDef({STATE_IDLE, STATE_IN_PROGRESS_MANUAL_DRAG, STATE_IN_PROGRESS_SMOOTH_SCROLL, + STATE_IN_PROGRESS_IMMEDIATE_SCROLL, STATE_IN_PROGRESS_FAKE_DRAG}) + private @interface AdapterState { + } + private static final int STATE_IDLE = 0; private static final int STATE_IN_PROGRESS_MANUAL_DRAG = 1; private static final int STATE_IN_PROGRESS_SMOOTH_SCROLL = 2; private static final int STATE_IN_PROGRESS_IMMEDIATE_SCROLL = 3; private static final int STATE_IN_PROGRESS_FAKE_DRAG = 4; + private static final int NO_POSITION = -1; - private final @NonNull - ViewPager2 mViewPager; - private final @NonNull - RecyclerView mRecyclerView; - private final @NonNull - LinearLayoutManager mLayoutManager; - private final ScrollEventValues mScrollValues; + private OnPageChangeCallback mCallback; + private final @NonNull ViewPager2 mViewPager; + private final @NonNull RecyclerView mRecyclerView; + private final @NonNull LinearLayoutManager mLayoutManager; + // state related fields - private @AdapterState - int mAdapterState; - private @ScrollState - int mScrollState; + private @AdapterState int mAdapterState; + private @ViewPager2.ScrollState int mScrollState; + private ScrollEventValues mScrollValues; private int mDragStartPosition; private int mTarget; private boolean mDispatchSelected; @@ -363,7 +368,7 @@ boolean isIdle() { /** * @return {@code true} if the ViewPager2 is being dragged. Returns {@code false} from the - * moment the ViewPager2 starts settling or goes idle. + * moment the ViewPager2 starts settling or goes idle. */ boolean isDragging() { return mScrollState == SCROLL_STATE_DRAGGING; @@ -371,7 +376,7 @@ boolean isDragging() { /** * @return {@code true} if a fake drag is ongoing. Returns {@code false} from the moment the - * {@link ViewPager2#endFakeDrag()} is called. + * {@link ViewPager2#endFakeDrag()} is called. */ boolean isFakeDragging() { return mFakeDragging; @@ -379,9 +384,8 @@ boolean isFakeDragging() { /** * Checks if the adapter state (not the scroll state) is in the manual or fake dragging state. - * * @return {@code true} if {@link #mAdapterState} is either {@link - * #STATE_IN_PROGRESS_MANUAL_DRAG} or {@link #STATE_IN_PROGRESS_FAKE_DRAG} + * #STATE_IN_PROGRESS_MANUAL_DRAG} or {@link #STATE_IN_PROGRESS_FAKE_DRAG} */ private boolean isInAnyDraggingState() { return mAdapterState == STATE_IN_PROGRESS_MANUAL_DRAG @@ -438,15 +442,6 @@ private int getPosition() { return mLayoutManager.findFirstVisibleItemPosition(); } - /** - * @hide - */ - @Retention(SOURCE) - @IntDef({STATE_IDLE, STATE_IN_PROGRESS_MANUAL_DRAG, STATE_IN_PROGRESS_SMOOTH_SCROLL, - STATE_IN_PROGRESS_IMMEDIATE_SCROLL, STATE_IN_PROGRESS_FAKE_DRAG}) - private @interface AdapterState { - } - private static final class ScrollEventValues { int mPosition; float mOffset; diff --git a/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java b/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java index 295e15753..16694f697 100644 --- a/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java +++ b/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java @@ -75,44 +75,69 @@ * @see androidx.viewpager.widget.ViewPager */ public final class ViewPager2 extends ViewGroup { + /** @hide */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + @Retention(SOURCE) + @IntDef({ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL}) + public @interface Orientation { + } + public static final int ORIENTATION_HORIZONTAL = RecyclerView.HORIZONTAL; public static final int ORIENTATION_VERTICAL = RecyclerView.VERTICAL; + + /** @hide */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + @Retention(SOURCE) + @IntDef({SCROLL_STATE_IDLE, SCROLL_STATE_DRAGGING, SCROLL_STATE_SETTLING}) + public @interface ScrollState { + } + + /** @hide */ + @SuppressWarnings("WeakerAccess") + @RestrictTo(LIBRARY_GROUP_PREFIX) + @Retention(SOURCE) + @IntDef({OFFSCREEN_PAGE_LIMIT_DEFAULT}) + @IntRange(from = 1) + public @interface OffscreenPageLimit { + } + /** * Indicates that the ViewPager2 is in an idle, settled state. The current page * is fully in view and no animation is in progress. */ public static final int SCROLL_STATE_IDLE = 0; + /** * Indicates that the ViewPager2 is currently being dragged by the user, or programmatically * via fake drag functionality. */ public static final int SCROLL_STATE_DRAGGING = 1; + /** * Indicates that the ViewPager2 is in the process of settling to a final position. */ public static final int SCROLL_STATE_SETTLING = 2; + /** * Value to indicate that the default caching mechanism of RecyclerView should be used instead * of explicitly prefetch and retain pages to either side of the current page. - * * @see #setOffscreenPageLimit(int) */ public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1; - /** - * Feature flag while stabilizing enhanced a11y - */ - static final boolean sFeatureEnhancedA11yEnabled = true; + + /** Feature flag while stabilizing enhanced a11y */ + static boolean sFeatureEnhancedA11yEnabled = true; + // reused in layout(...) private final Rect mTmpContainerRect = new Rect(); private final Rect mTmpChildRect = new Rect(); - private final CompositeOnPageChangeCallback mExternalPageChangeCallbacks = + + private CompositeOnPageChangeCallback mExternalPageChangeCallbacks = new CompositeOnPageChangeCallback(3); + int mCurrentItem; - boolean mCurrentItemDirty; - LinearLayoutManager mLayoutManager; // to avoid creation of a synthetic accessor - RecyclerView mRecyclerView; - ScrollEventAdapter mScrollEventAdapter; - private final RecyclerView.AdapterDataObserver mCurrentItemDataSetChangeObserver = + boolean mCurrentItemDirty = false; + private RecyclerView.AdapterDataObserver mCurrentItemDataSetChangeObserver = new DataSetChangeObserver() { @Override public void onChanged() { @@ -120,18 +145,21 @@ public void onChanged() { mScrollEventAdapter.notifyDataSetChangeHappened(); } }; - AccessibilityProvider mAccessibilityProvider; // to avoid creation of a synthetic accessor + + LinearLayoutManager mLayoutManager; // to avoid creation of a synthetic accessor private int mPendingCurrentItem = NO_POSITION; private Parcelable mPendingAdapterState; + RecyclerView mRecyclerView; private PagerSnapHelper mPagerSnapHelper; + ScrollEventAdapter mScrollEventAdapter; private CompositeOnPageChangeCallback mPageChangeEventDispatcher; private FakeDrag mFakeDragger; private PageTransformerAdapter mPageTransformerAdapter; - private RecyclerView.ItemAnimator mSavedItemAnimator; - private boolean mSavedItemAnimatorPresent; + private RecyclerView.ItemAnimator mSavedItemAnimator = null; + private boolean mSavedItemAnimatorPresent = false; private boolean mUserInputEnabled = true; - private @OffscreenPageLimit - int mOffscreenPageLimit = OFFSCREEN_PAGE_LIMIT_DEFAULT; + private @OffscreenPageLimit int mOffscreenPageLimit = OFFSCREEN_PAGE_LIMIT_DEFAULT; + AccessibilityProvider mAccessibilityProvider; // to avoid creation of a synthetic accessor public ViewPager2(@NonNull Context context) { super(context); @@ -151,7 +179,7 @@ public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs, int de @RequiresApi(21) @SuppressLint("ClassVerificationFailure") public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, - int defStyleRes) { + int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initialize(context, attrs); } @@ -190,7 +218,7 @@ private void initialize(Context context, AttributeSet attrs) { // Callback that updates mCurrentItem after swipes. Also triggered in other cases, but in // all those cases mCurrentItem will only be overwritten with the same value. - OnPageChangeCallback currentItemUpdater = new OnPageChangeCallback() { + final OnPageChangeCallback currentItemUpdater = new OnPageChangeCallback() { @Override public void onPageSelected(int position) { if (mCurrentItem != position) { @@ -208,7 +236,7 @@ public void onPageScrollStateChanged(int newState) { }; // Prevents focus from remaining on a no-longer visible page - OnPageChangeCallback focusClearer = new OnPageChangeCallback() { + final OnPageChangeCallback focusClearer = new OnPageChangeCallback() { @Override public void onPageSelected(int position) { clearFocus(); @@ -237,7 +265,7 @@ public void onPageSelected(int position) { /** * A lot of places in code rely on an assumption that the page fills the whole ViewPager2. - *

    + * * TODO(b/70666617) Allow page width different than width/height 100%/100% */ private RecyclerView.OnChildAttachStateChangeListener enforceChildFillListener() { @@ -270,12 +298,12 @@ public CharSequence getAccessibilityClassName() { } private void setOrientation(Context context, AttributeSet attrs) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewPager2); - ViewCompat.saveAttributeDataForStyleable(this, context, R.styleable.ViewPager2, attrs, + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView); + ViewCompat.saveAttributeDataForStyleable(this, context, R.styleable.RecyclerView, attrs, a, 0, 0); try { setOrientation( - a.getInt(R.styleable.ViewPager2_android_orientation, ORIENTATION_HORIZONTAL)); + a.getInt(R.styleable.RecyclerView_android_orientation, ORIENTATION_HORIZONTAL)); } finally { a.recycle(); } @@ -343,8 +371,8 @@ protected void dispatchRestoreInstanceState(SparseArray container) { // RecyclerView changed an id, so we need to reflect that in the saved state Parcelable state = container.get(getId()); if (state instanceof SavedState) { - int previousRvId = ((SavedState) state).mRecyclerViewId; - int currentRvId = mRecyclerView.getId(); + final int previousRvId = ((SavedState) state).mRecyclerViewId; + final int currentRvId = mRecyclerView.getId(); container.put(currentRvId, container.get(previousRvId)); container.remove(previousRvId); } @@ -355,22 +383,60 @@ protected void dispatchRestoreInstanceState(SparseArray container) { restorePendingState(); } - private void registerCurrentItemDataSetTracker(@Nullable Adapter adapter) { - if (adapter != null) { - adapter.registerAdapterDataObserver(mCurrentItemDataSetChangeObserver); + static class SavedState extends BaseSavedState { + int mRecyclerViewId; + int mCurrentItem; + Parcelable mAdapterState; + + @RequiresApi(24) + @SuppressLint("ClassVerificationFailure") + SavedState(Parcel source, ClassLoader loader) { + super(source, loader); + readValues(source, loader); } - } - private void unregisterCurrentItemDataSetTracker(@Nullable Adapter adapter) { - if (adapter != null) { - adapter.unregisterAdapterDataObserver(mCurrentItemDataSetChangeObserver); + SavedState(Parcel source) { + super(source); + readValues(source, null); } - } - @SuppressWarnings("rawtypes") - public @Nullable - Adapter getAdapter() { - return mRecyclerView.getAdapter(); + SavedState(Parcelable superState) { + super(superState); + } + + @SuppressWarnings("deprecation") + private void readValues(Parcel source, ClassLoader loader) { + mRecyclerViewId = source.readInt(); + mCurrentItem = source.readInt(); + mAdapterState = source.readParcelable(loader); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(mRecyclerViewId); + out.writeInt(mCurrentItem); + out.writeParcelable(mAdapterState, flags); + } + + public static final Creator CREATOR = new ClassLoaderCreator() { + @Override + public SavedState createFromParcel(Parcel source, ClassLoader loader) { + return Build.VERSION.SDK_INT >= 24 + ? new SavedState(source, loader) + : new SavedState(source); + } + + @Override + public SavedState createFromParcel(Parcel source) { + return createFromParcel(source, null); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; } /** @@ -399,7 +465,7 @@ Adapter getAdapter() { * @see RecyclerView#setAdapter(Adapter) */ public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) { - Adapter currentAdapter = mRecyclerView.getAdapter(); + final Adapter currentAdapter = mRecyclerView.getAdapter(); mAccessibilityProvider.onDetachAdapter(currentAdapter); unregisterCurrentItemDataSetTracker(currentAdapter); mRecyclerView.setAdapter(adapter); @@ -409,6 +475,23 @@ public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) registerCurrentItemDataSetTracker(adapter); } + private void registerCurrentItemDataSetTracker(@Nullable Adapter adapter) { + if (adapter != null) { + adapter.registerAdapterDataObserver(mCurrentItemDataSetChangeObserver); + } + } + + private void unregisterCurrentItemDataSetTracker(@Nullable Adapter adapter) { + if (adapter != null) { + adapter.unregisterAdapterDataObserver(mCurrentItemDataSetChangeObserver); + } + } + + @SuppressWarnings("rawtypes") + public @Nullable Adapter getAdapter() { + return mRecyclerView.getAdapter(); + } + @Override public void onViewAdded(View child) { // TODO(b/70666620): consider adding a support for Decor views @@ -459,9 +542,7 @@ protected void onLayout(boolean changed, int l, int t, int r, int b) { } } - /** - * Updates {@link #mCurrentItem} based on what is currently visible in the viewport. - */ + /** Updates {@link #mCurrentItem} based on what is currently visible in the viewport. */ void updateCurrentItem() { if (mPagerSnapHelper == null) { throw new IllegalStateException("Design assumption violated."); @@ -482,18 +563,12 @@ void updateCurrentItem() { } int getPageSize() { - RecyclerView rv = mRecyclerView; + final RecyclerView rv = mRecyclerView; return getOrientation() == ORIENTATION_HORIZONTAL ? rv.getWidth() - rv.getPaddingLeft() - rv.getPaddingRight() : rv.getHeight() - rv.getPaddingTop() - rv.getPaddingBottom(); } - public @Orientation - int getOrientation() { - return mLayoutManager.getOrientation() == LinearLayoutManager.VERTICAL - ? ORIENTATION_VERTICAL : ORIENTATION_HORIZONTAL; - } - /** * Sets the orientation of the ViewPager2. * @@ -504,16 +579,33 @@ public void setOrientation(@Orientation int orientation) { mAccessibilityProvider.onSetOrientation(); } + public @Orientation int getOrientation() { + return mLayoutManager.getOrientation() == LinearLayoutManager.VERTICAL + ? ViewPager2.ORIENTATION_VERTICAL : ViewPager2.ORIENTATION_HORIZONTAL; + } + boolean isRtl() { return mLayoutManager.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL; } + /** + * Set the currently selected page. If the ViewPager has already been through its first + * layout with its current adapter there will be a smooth animated transition between + * the current item and the specified item. Silently ignored if the adapter is not set or + * empty. Clamps item to the bounds of the adapter. + * + * @param item Item index to select + */ + public void setCurrentItem(int item) { + setCurrentItem(item, true); + } + /** * Set the currently selected page. If {@code smoothScroll = true}, will perform a smooth * animation from the current item to the new item. Silently ignored if the adapter is not set * or empty. Clamps item to the bounds of the adapter. * - * @param item Item index to select + * @param item Item index to select * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately */ public void setCurrentItem(int item, boolean smoothScroll) { @@ -592,24 +684,12 @@ public int getCurrentItem() { return mCurrentItem; } - /** - * Set the currently selected page. If the ViewPager has already been through its first - * layout with its current adapter there will be a smooth animated transition between - * the current item and the specified item. Silently ignored if the adapter is not set or - * empty. Clamps item to the bounds of the adapter. - * - * @param item Item index to select - */ - public void setCurrentItem(int item) { - setCurrentItem(item, true); - } - /** * Returns the current scroll state of the ViewPager2. Returned value is one of can be one of * {@link #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}. * * @return The scroll state that was last dispatched to {@link - * OnPageChangeCallback#onPageScrollStateChanged(int)} + * OnPageChangeCallback#onPageScrollStateChanged(int)} */ @ScrollState public int getScrollState() { @@ -632,7 +712,8 @@ public int getScrollState() { * drag is already in progress, this method will return {@code false}. * * @return {@code true} if the fake drag began successfully, {@code false} if it could not be - * started + * started + * * @see #fakeDragBy(float) * @see #endFakeDrag() * @see #isFakeDragging() @@ -653,7 +734,8 @@ public boolean beginFakeDrag() { * * @param offsetPxFloat Offset in pixels to drag by * @return {@code true} if the fake drag was executed. If {@code false} is returned, it means - * there was no fake drag to end. + * there was no fake drag to end. + * * @see #beginFakeDrag() * @see #endFakeDrag() * @see #isFakeDragging() @@ -666,7 +748,8 @@ public boolean fakeDragBy(@SuppressLint("SupportAnnotationUsage") @Px float offs * End a fake drag of the pager. * * @return {@code true} if the fake drag was ended. If {@code false} is returned, it means there - * was no fake drag to end. + * was no fake drag to end. + * * @see #beginFakeDrag() * @see #fakeDragBy(float) * @see #isFakeDragging() @@ -704,16 +787,6 @@ void snapToPage() { } } - /** - * Returns if user initiated scrolling between pages is enabled. Enabled by default. - * - * @return {@code true} if users can scroll the ViewPager2, {@code false} otherwise - * @see #setUserInputEnabled(boolean) - */ - public boolean isUserInputEnabled() { - return mUserInputEnabled; - } - /** * Enable or disable user initiated scrolling. This includes touch input (scroll and fling * gestures) and accessibility input. Disabling keyboard input is not yet supported. When user @@ -721,7 +794,7 @@ public boolean isUserInputEnabled() { * boolean) setCurrentItem} still work. By default, user initiated scrolling is enabled. * * @param enabled {@code true} to allow user initiated scrolling, {@code false} to block user - * initiated scrolling + * initiated scrolling * @see #isUserInputEnabled() */ public void setUserInputEnabled(boolean enabled) { @@ -730,15 +803,13 @@ public void setUserInputEnabled(boolean enabled) { } /** - * Returns the number of pages that will be retained to either side of the current page in the - * view hierarchy in an idle state. Defaults to {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT}. + * Returns if user initiated scrolling between pages is enabled. Enabled by default. * - * @return How many pages will be kept offscreen on either side - * @see #setOffscreenPageLimit(int) + * @return {@code true} if users can scroll the ViewPager2, {@code false} otherwise + * @see #setUserInputEnabled(boolean) */ - @OffscreenPageLimit - public int getOffscreenPageLimit() { - return mOffscreenPageLimit; + public boolean isUserInputEnabled() { + return mUserInputEnabled; } /** @@ -762,7 +833,7 @@ public int getOffscreenPageLimit() { * it is set to {@code OFFSCREEN_PAGE_LIMIT_DEFAULT}.

    * * @param limit How many pages will be kept offscreen on either side. Valid values are all - * values {@code >= 1} and {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT} + * values {@code >= 1} and {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT} * @throws IllegalArgumentException If the given limit is invalid * @see #getOffscreenPageLimit() */ @@ -776,6 +847,18 @@ public void setOffscreenPageLimit(@OffscreenPageLimit int limit) { mRecyclerView.requestLayout(); } + /** + * Returns the number of pages that will be retained to either side of the current page in the + * view hierarchy in an idle state. Defaults to {@link #OFFSCREEN_PAGE_LIMIT_DEFAULT}. + * + * @return How many pages will be kept offscreen on either side + * @see #setOffscreenPageLimit(int) + */ + @OffscreenPageLimit + public int getOffscreenPageLimit() { + return mOffscreenPageLimit; + } + @Override public boolean canScrollHorizontally(int direction) { return mRecyclerView.canScrollHorizontally(direction); @@ -818,6 +901,7 @@ public void unregisterOnPageChangeCallback(@NonNull OnPageChangeCallback callbac * data-set change animations. * * @param transformer PageTransformer that will modify each page's animation properties + * * @see MarginPageTransformer * @see CompositePageTransformer */ @@ -885,194 +969,106 @@ public boolean performAccessibilityAction(int action, @Nullable Bundle arguments } /** - * Add an {@link ItemDecoration} to this ViewPager2. Item decorations can - * affect both measurement and drawing of individual item views. - * - *

    Item decorations are ordered. Decorations placed earlier in the list will - * be run/queried/drawn first for their effects on item views. Padding added to views - * will be nested; a padding added by an earlier decoration will mean further - * item decorations in the list will be asked to draw/pad within the previous decoration's - * given area.

    - * - * @param decor Decoration to add + * Slightly modified RecyclerView to get ViewPager behavior in accessibility and to + * enable/disable user scrolling. */ - public void addItemDecoration(@NonNull ItemDecoration decor) { - mRecyclerView.addItemDecoration(decor); - } + private class RecyclerViewImpl extends RecyclerView { + RecyclerViewImpl(@NonNull Context context) { + super(context); + } - /** - * Add an {@link ItemDecoration} to this ViewPager2. Item decorations can - * affect both measurement and drawing of individual item views. - * - *

    Item decorations are ordered. Decorations placed earlier in the list will - * be run/queried/drawn first for their effects on item views. Padding added to views - * will be nested; a padding added by an earlier decoration will mean further - * item decorations in the list will be asked to draw/pad within the previous decoration's - * given area.

    - * - * @param decor Decoration to add - * @param index Position in the decoration chain to insert this decoration at. If this value - * is negative the decoration will be added at the end. - * @throws IndexOutOfBoundsException on indexes larger than {@link #getItemDecorationCount} - */ - public void addItemDecoration(@NonNull ItemDecoration decor, int index) { - mRecyclerView.addItemDecoration(decor, index); - } + @RequiresApi(23) + @Override + public CharSequence getAccessibilityClassName() { + if (mAccessibilityProvider.handlesRvGetAccessibilityClassName()) { + return mAccessibilityProvider.onRvGetAccessibilityClassName(); + } + return super.getAccessibilityClassName(); + } - /** - * Returns an {@link ItemDecoration} previously added to this ViewPager2. - * - * @param index The index position of the desired ItemDecoration. - * @return the ItemDecoration at index position - * @throws IndexOutOfBoundsException on invalid index - */ - @NonNull - public ItemDecoration getItemDecorationAt(int index) { - return mRecyclerView.getItemDecorationAt(index); - } + @Override + public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setFromIndex(mCurrentItem); + event.setToIndex(mCurrentItem); + mAccessibilityProvider.onRvInitializeAccessibilityEvent(event); + } - /** - * Returns the number of {@link ItemDecoration} currently added to this ViewPager2. - * - * @return number of ItemDecorations currently added added to this ViewPager2. - */ - public int getItemDecorationCount() { - return mRecyclerView.getItemDecorationCount(); - } - - /** - * Invalidates all ItemDecorations. If ViewPager2 has item decorations, calling this method - * will trigger a {@link #requestLayout()} call. - */ - public void invalidateItemDecorations() { - mRecyclerView.invalidateItemDecorations(); - } - - /** - * Removes the {@link ItemDecoration} associated with the supplied index position. - * - * @param index The index position of the ItemDecoration to be removed. - * @throws IndexOutOfBoundsException on invalid index - */ - public void removeItemDecorationAt(int index) { - mRecyclerView.removeItemDecorationAt(index); - } - - /** - * Remove an {@link ItemDecoration} from this ViewPager2. - * - *

    The given decoration will no longer impact the measurement and drawing of - * item views.

    - * - * @param decor Decoration to remove - * @see #addItemDecoration(ItemDecoration) - */ - public void removeItemDecoration(@NonNull ItemDecoration decor) { - mRecyclerView.removeItemDecoration(decor); - } - - /** - * @hide - */ - @RestrictTo(LIBRARY_GROUP_PREFIX) - @Retention(SOURCE) - @IntDef({ORIENTATION_HORIZONTAL, ORIENTATION_VERTICAL}) - public @interface Orientation { - } - - /** - * @hide - */ - @RestrictTo(LIBRARY_GROUP_PREFIX) - @Retention(SOURCE) - @IntDef({SCROLL_STATE_IDLE, SCROLL_STATE_DRAGGING, SCROLL_STATE_SETTLING}) - public @interface ScrollState { - } - - /** - * @hide - */ - @SuppressWarnings("WeakerAccess") - @RestrictTo(LIBRARY_GROUP_PREFIX) - @Retention(SOURCE) - @IntDef(OFFSCREEN_PAGE_LIMIT_DEFAULT) - @IntRange(from = 1) - public @interface OffscreenPageLimit { - } - - /** - * A PageTransformer is invoked whenever a visible/attached page is scrolled. - * This offers an opportunity for the application to apply a custom transformation - * to the page views using animation properties. - */ - public interface PageTransformer { + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + return isUserInputEnabled() && super.onTouchEvent(event); + } - /** - * Apply a property transformation to the given page. - * - * @param page Apply the transformation to this page - * @param position Position of page relative to the current front-and-center - * position of the pager. 0 is front and center. 1 is one full - * page position to the right, and -2 is two pages to the left. - * Minimum / maximum observed values depend on how many pages we keep - * attached, which depends on offscreenPageLimit. - * @see #setOffscreenPageLimit(int) - */ - void transformPage(@NonNull View page, float position); + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return isUserInputEnabled() && super.onInterceptTouchEvent(ev); + } } - static class SavedState extends BaseSavedState { - public static final Creator CREATOR = new ClassLoaderCreator() { - @Override - public SavedState createFromParcel(Parcel source, ClassLoader loader) { - return Build.VERSION.SDK_INT >= 24 - ? new SavedState(source, loader) - : new SavedState(source); - } + private class LinearLayoutManagerImpl extends LinearLayoutManager { + LinearLayoutManagerImpl(Context context) { + super(context); + } - @Override - public SavedState createFromParcel(Parcel source) { - return createFromParcel(source, null); + @Override + public boolean performAccessibilityAction(@NonNull RecyclerView.Recycler recycler, + @NonNull RecyclerView.State state, int action, @Nullable Bundle args) { + if (mAccessibilityProvider.handlesLmPerformAccessibilityAction(action)) { + return mAccessibilityProvider.onLmPerformAccessibilityAction(action); } + return super.performAccessibilityAction(recycler, state, action, args); + } - @Override - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; - int mRecyclerViewId; - int mCurrentItem; - Parcelable mAdapterState; + @Override + public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler, + @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(recycler, state, info); + mAccessibilityProvider.onLmInitializeAccessibilityNodeInfo(info); + } - @RequiresApi(24) - @SuppressLint("ClassVerificationFailure") - SavedState(Parcel source, ClassLoader loader) { - super(source, loader); - readValues(source, loader); + @Override + public void onInitializeAccessibilityNodeInfoForItem( + @NonNull RecyclerView.Recycler recycler, + @NonNull RecyclerView.State state, @NonNull View host, + @NonNull AccessibilityNodeInfoCompat info) { + mAccessibilityProvider.onLmInitializeAccessibilityNodeInfoForItem(host, info); } - SavedState(Parcel source) { - super(source); - readValues(source, null); + @Override + protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state, + @NonNull int[] extraLayoutSpace) { + int pageLimit = getOffscreenPageLimit(); + if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) { + // Only do custom prefetching of offscreen pages if requested + super.calculateExtraLayoutSpace(state, extraLayoutSpace); + return; + } + final int offscreenSpace = getPageSize() * pageLimit; + extraLayoutSpace[0] = offscreenSpace; + extraLayoutSpace[1] = offscreenSpace; } - SavedState(Parcelable superState) { - super(superState); + @Override + public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, + @NonNull View child, @NonNull Rect rect, boolean immediate, + boolean focusedChildVisible) { + return false; // users should use setCurrentItem instead } + } - @SuppressWarnings("deprecation") - private void readValues(Parcel source, ClassLoader loader) { - mRecyclerViewId = source.readInt(); - mCurrentItem = source.readInt(); - mAdapterState = source.readParcelable(loader); + private class PagerSnapHelperImpl extends PagerSnapHelper { + PagerSnapHelperImpl() { } + @Nullable @Override - public void writeToParcel(Parcel out, int flags) { - super.writeToParcel(out, flags); - out.writeInt(mRecyclerViewId); - out.writeInt(mCurrentItem); - out.writeParcelable(mAdapterState, flags); + public View findSnapView(RecyclerView.LayoutManager layoutManager) { + // When interrupting a smooth scroll with a fake drag, we stop RecyclerView's scroll + // animation, which fires a scroll state change to IDLE. PagerSnapHelper then kicks in + // to snap to a page, which we need to prevent here. + // Simplifying that case: during a fake drag, no snapping should occur. + return isFakeDragging() ? null : super.findSnapView(layoutManager); } } @@ -1099,13 +1095,13 @@ public abstract static class OnPageChangeCallback { * This method will be invoked when the current page is scrolled, either as part * of a programmatically initiated smooth scroll or a user initiated touch scroll. * - * @param position Position index of the first page currently being displayed. - * Page position+1 will be visible if positionOffset is nonzero. - * @param positionOffset Value from [0, 1) indicating the offset from the page at position. + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param positionOffset Value from [0, 1) indicating the offset from the page at position. * @param positionOffsetPixels Value in pixels indicating the offset from position. */ public void onPageScrolled(int position, float positionOffset, - @Px int positionOffsetPixels) { + @Px int positionOffsetPixels) { } /** @@ -1128,149 +1124,119 @@ public void onPageScrollStateChanged(@ScrollState int state) { } /** - * Simplified {@link RecyclerView.AdapterDataObserver} for clients interested in any data-set - * changes regardless of their nature. + * A PageTransformer is invoked whenever a visible/attached page is scrolled. + * This offers an opportunity for the application to apply a custom transformation + * to the page views using animation properties. */ - private abstract static class DataSetChangeObserver extends RecyclerView.AdapterDataObserver { - @Override - public abstract void onChanged(); - - @Override - public final void onItemRangeChanged(int positionStart, int itemCount) { - onChanged(); - } - - @Override - public final void onItemRangeChanged(int positionStart, int itemCount, - @Nullable Object payload) { - onChanged(); - } - - @Override - public final void onItemRangeInserted(int positionStart, int itemCount) { - onChanged(); - } - - @Override - public final void onItemRangeRemoved(int positionStart, int itemCount) { - onChanged(); - } + public interface PageTransformer { - @Override - public final void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { - onChanged(); - } + /** + * Apply a property transformation to the given page. + * + * @param page Apply the transformation to this page + * @param position Position of page relative to the current front-and-center + * position of the pager. 0 is front and center. 1 is one full + * page position to the right, and -2 is two pages to the left. + * Minimum / maximum observed values depend on how many pages we keep + * attached, which depends on offscreenPageLimit. + * + * @see #setOffscreenPageLimit(int) + */ + void transformPage(@NonNull View page, float position); } /** - * Slightly modified RecyclerView to get ViewPager behavior in accessibility and to - * enable/disable user scrolling. + * Add an {@link ItemDecoration} to this ViewPager2. Item decorations can + * affect both measurement and drawing of individual item views. + * + *

    Item decorations are ordered. Decorations placed earlier in the list will + * be run/queried/drawn first for their effects on item views. Padding added to views + * will be nested; a padding added by an earlier decoration will mean further + * item decorations in the list will be asked to draw/pad within the previous decoration's + * given area.

    + * + * @param decor Decoration to add */ - private class RecyclerViewImpl extends RecyclerView { - RecyclerViewImpl(@NonNull Context context) { - super(context); - } - - @RequiresApi(23) - @Override - public CharSequence getAccessibilityClassName() { - if (mAccessibilityProvider.handlesRvGetAccessibilityClassName()) { - return mAccessibilityProvider.onRvGetAccessibilityClassName(); - } - return super.getAccessibilityClassName(); - } - - @Override - public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setFromIndex(mCurrentItem); - event.setToIndex(mCurrentItem); - mAccessibilityProvider.onRvInitializeAccessibilityEvent(event); - } - - @SuppressLint("ClickableViewAccessibility") - @Override - public boolean onTouchEvent(MotionEvent event) { - return isUserInputEnabled() && super.onTouchEvent(event); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - return isUserInputEnabled() && super.onInterceptTouchEvent(ev); - } + public void addItemDecoration(@NonNull ItemDecoration decor) { + mRecyclerView.addItemDecoration(decor); } - private class LinearLayoutManagerImpl extends LinearLayoutManager { - LinearLayoutManagerImpl(Context context) { - super(context); - } - - @Override - public boolean performAccessibilityAction(@NonNull RecyclerView.Recycler recycler, - @NonNull RecyclerView.State state, int action, @Nullable Bundle args) { - if (mAccessibilityProvider.handlesLmPerformAccessibilityAction(action)) { - return mAccessibilityProvider.onLmPerformAccessibilityAction(action); - } - return super.performAccessibilityAction(recycler, state, action, args); - } - - @Override - public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler, - @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) { - super.onInitializeAccessibilityNodeInfo(recycler, state, info); - mAccessibilityProvider.onLmInitializeAccessibilityNodeInfo(info); - } + /** + * Add an {@link ItemDecoration} to this ViewPager2. Item decorations can + * affect both measurement and drawing of individual item views. + * + *

    Item decorations are ordered. Decorations placed earlier in the list will + * be run/queried/drawn first for their effects on item views. Padding added to views + * will be nested; a padding added by an earlier decoration will mean further + * item decorations in the list will be asked to draw/pad within the previous decoration's + * given area.

    + * + * @param decor Decoration to add + * @param index Position in the decoration chain to insert this decoration at. If this value + * is negative the decoration will be added at the end. + * @throws IndexOutOfBoundsException on indexes larger than {@link #getItemDecorationCount} + */ + public void addItemDecoration(@NonNull ItemDecoration decor, int index) { + mRecyclerView.addItemDecoration(decor, index); + } - @Override - public void onInitializeAccessibilityNodeInfoForItem( - @NonNull RecyclerView.Recycler recycler, - @NonNull RecyclerView.State state, @NonNull View host, - @NonNull AccessibilityNodeInfoCompat info) { - mAccessibilityProvider.onLmInitializeAccessibilityNodeInfoForItem(host, info); - } + /** + * Returns an {@link ItemDecoration} previously added to this ViewPager2. + * + * @param index The index position of the desired ItemDecoration. + * @return the ItemDecoration at index position + * @throws IndexOutOfBoundsException on invalid index + */ + @NonNull + public ItemDecoration getItemDecorationAt(int index) { + return mRecyclerView.getItemDecorationAt(index); + } - @Override - protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state, - @NonNull int[] extraLayoutSpace) { - int pageLimit = getOffscreenPageLimit(); - if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) { - // Only do custom prefetching of offscreen pages if requested - super.calculateExtraLayoutSpace(state, extraLayoutSpace); - return; - } - int offscreenSpace = getPageSize() * pageLimit; - extraLayoutSpace[0] = offscreenSpace; - extraLayoutSpace[1] = offscreenSpace; - } + /** + * Returns the number of {@link ItemDecoration} currently added to this ViewPager2. + * + * @return number of ItemDecorations currently added added to this ViewPager2. + */ + public int getItemDecorationCount() { + return mRecyclerView.getItemDecorationCount(); + } - @Override - public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, - @NonNull View child, @NonNull Rect rect, boolean immediate, - boolean focusedChildVisible) { - return false; // users should use setCurrentItem instead - } + /** + * Invalidates all ItemDecorations. If ViewPager2 has item decorations, calling this method + * will trigger a {@link #requestLayout()} call. + */ + public void invalidateItemDecorations() { + mRecyclerView.invalidateItemDecorations(); } - private class PagerSnapHelperImpl extends PagerSnapHelper { - PagerSnapHelperImpl() { - } + /** + * Removes the {@link ItemDecoration} associated with the supplied index position. + * + * @param index The index position of the ItemDecoration to be removed. + * @throws IndexOutOfBoundsException on invalid index + */ + public void removeItemDecorationAt(int index) { + mRecyclerView.removeItemDecorationAt(index); + } - @Nullable - @Override - public View findSnapView(RecyclerView.LayoutManager layoutManager) { - // When interrupting a smooth scroll with a fake drag, we stop RecyclerView's scroll - // animation, which fires a scroll state change to IDLE. PagerSnapHelper then kicks in - // to snap to a page, which we need to prevent here. - // Simplifying that case: during a fake drag, no snapping should occur. - return isFakeDragging() ? null : super.findSnapView(layoutManager); - } + /** + * Remove an {@link ItemDecoration} from this ViewPager2. + * + *

    The given decoration will no longer impact the measurement and drawing of + * item views.

    + * + * @param decor Decoration to remove + * @see #addItemDecoration(ItemDecoration) + */ + public void removeItemDecoration(@NonNull ItemDecoration decor) { + mRecyclerView.removeItemDecoration(decor); } // TODO(b/141956012): Suppressed during upgrade to AGP 3.6. @SuppressWarnings({"ClassCanBeStatic", "InnerClassMayBeStatic"}) private abstract class AccessibilityProvider { void onInitialize(@NonNull CompositeOnPageChangeCallback pageChangeEventDispatcher, - @NonNull RecyclerView recyclerView) { + @NonNull RecyclerView recyclerView) { } boolean handlesGetAccessibilityClassName() { @@ -1328,7 +1294,7 @@ void onLmInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfoCompat in } void onLmInitializeAccessibilityNodeInfoForItem(@NonNull View host, - @NonNull AccessibilityNodeInfoCompat info) { + @NonNull AccessibilityNodeInfoCompat info) { } boolean handlesRvGetAccessibilityClassName() { @@ -1399,7 +1365,7 @@ class PageAwareAccessibilityProvider extends AccessibilityProvider { @Override public void onInitialize(@NonNull CompositeOnPageChangeCallback pageChangeEventDispatcher, - @NonNull RecyclerView recyclerView) { + @NonNull RecyclerView recyclerView) { ViewCompat.setImportantForAccessibility(recyclerView, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO); @@ -1484,7 +1450,7 @@ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { @Override void onLmInitializeAccessibilityNodeInfoForItem(@NonNull View host, - @NonNull AccessibilityNodeInfoCompat info) { + @NonNull AccessibilityNodeInfoCompat info) { addCollectionItemInfo(host, info); } @@ -1531,10 +1497,14 @@ void setCurrentItemFromAccessibilityCommand(int item) { void updatePageAccessibilityActions() { ViewPager2 viewPager = ViewPager2.this; - @SuppressLint("InlinedApi") final int actionIdPageLeft = android.R.id.accessibilityActionPageLeft; - @SuppressLint("InlinedApi") final int actionIdPageRight = android.R.id.accessibilityActionPageRight; - @SuppressLint("InlinedApi") final int actionIdPageUp = android.R.id.accessibilityActionPageUp; - @SuppressLint("InlinedApi") final int actionIdPageDown = android.R.id.accessibilityActionPageDown; + @SuppressLint("InlinedApi") + final int actionIdPageLeft = android.R.id.accessibilityActionPageLeft; + @SuppressLint("InlinedApi") + final int actionIdPageRight = android.R.id.accessibilityActionPageRight; + @SuppressLint("InlinedApi") + final int actionIdPageUp = android.R.id.accessibilityActionPageUp; + @SuppressLint("InlinedApi") + final int actionIdPageDown = android.R.id.accessibilityActionPageDown; ViewCompat.removeAccessibilityAction(viewPager, actionIdPageLeft); ViewCompat.removeAccessibilityAction(viewPager, actionIdPageRight); @@ -1616,7 +1586,7 @@ private void addCollectionItemInfo(View host, AccessibilityNodeInfoCompat infoCo } private void addScrollActions(AccessibilityNodeInfoCompat infoCompat) { - Adapter adapter = getAdapter(); + final Adapter adapter = getAdapter(); if (adapter == null) { return; } @@ -1633,4 +1603,39 @@ private void addScrollActions(AccessibilityNodeInfoCompat infoCompat) { infoCompat.setScrollable(true); } } + + /** + * Simplified {@link RecyclerView.AdapterDataObserver} for clients interested in any data-set + * changes regardless of their nature. + */ + private abstract static class DataSetChangeObserver extends RecyclerView.AdapterDataObserver { + @Override + public abstract void onChanged(); + + @Override + public final void onItemRangeChanged(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public final void onItemRangeChanged(int positionStart, int itemCount, + @Nullable Object payload) { + onChanged(); + } + + @Override + public final void onItemRangeInserted(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public final void onItemRangeRemoved(int positionStart, int itemCount) { + onChanged(); + } + + @Override + public final void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + onChanged(); + } + } } diff --git a/viewpager2/src/main/java/androidx/viewpager2/widget/WindowInsetsApplier.java b/viewpager2/src/main/java/androidx/viewpager2/widget/WindowInsetsApplier.java index ef37ba815..e3e2578d3 100644 --- a/viewpager2/src/main/java/androidx/viewpager2/widget/WindowInsetsApplier.java +++ b/viewpager2/src/main/java/androidx/viewpager2/widget/WindowInsetsApplier.java @@ -78,11 +78,11 @@ public static boolean install(@NonNull ViewPager2 viewPager) { @NonNull @Override public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, - @NonNull WindowInsetsCompat insets) { + @NonNull WindowInsetsCompat insets) { ViewPager2 viewPager = (ViewPager2) v; // First let the ViewPager2 itself try and consume them... - WindowInsetsCompat applied = ViewCompat.onApplyWindowInsets(viewPager, insets); + final WindowInsetsCompat applied = ViewCompat.onApplyWindowInsets(viewPager, insets); if (applied.isConsumed()) { // If the ViewPager2 consumed all insets, return now @@ -96,7 +96,7 @@ public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, // manually dispatch the applied insets, not allowing children to consume // them from each other, making a copy for every invocation - RecyclerView rv = viewPager.mRecyclerView; + final RecyclerView rv = viewPager.mRecyclerView; for (int i = 0, count = rv.getChildCount(); i < count; i++) { // We don't care about b/168984101 here, as we're not using the return value ViewCompat.dispatchApplyWindowInsets(rv.getChildAt(i), new WindowInsetsCompat(applied)); diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/LinearLayoutManager_SavedState.kt b/viewpager2/src/main/kotlin/androidx/recyclerview/widget/LinearLayoutManager_SavedState.kt similarity index 68% rename from viewpager2/src/main/java/androidx/recyclerview/widget/LinearLayoutManager_SavedState.kt rename to viewpager2/src/main/kotlin/androidx/recyclerview/widget/LinearLayoutManager_SavedState.kt index 671b4c423..6964b6b1a 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/LinearLayoutManager_SavedState.kt +++ b/viewpager2/src/main/kotlin/androidx/recyclerview/widget/LinearLayoutManager_SavedState.kt @@ -9,8 +9,13 @@ import kotlinx.serialization.Serializable */ @Serializable class LinearLayoutManager_SavedState : Parcelable { + @JvmField var mAnchorPosition = 0 + + @JvmField var mAnchorOffset = 0 + + @JvmField var mAnchorLayoutFromEnd = false @Suppress("unused") @@ -45,17 +50,13 @@ class LinearLayoutManager_SavedState : Parcelable { dest.writeInt(if (mAnchorLayoutFromEnd) 1 else 0) } - companion object { - @JvmField - val CREATOR: Parcelable.Creator = - object : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): LinearLayoutManager_SavedState { - return LinearLayoutManager_SavedState(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): LinearLayoutManager_SavedState { + return LinearLayoutManager_SavedState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } } } diff --git a/viewpager2/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager_SavedState.kt b/viewpager2/src/main/kotlin/androidx/recyclerview/widget/StaggeredGridLayoutManager_SavedState.kt similarity index 89% rename from viewpager2/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager_SavedState.kt rename to viewpager2/src/main/kotlin/androidx/recyclerview/widget/StaggeredGridLayoutManager_SavedState.kt index 117167766..0ba8e968c 100644 --- a/viewpager2/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager_SavedState.kt +++ b/viewpager2/src/main/kotlin/androidx/recyclerview/widget/StaggeredGridLayoutManager_SavedState.kt @@ -10,16 +10,34 @@ import java.util.* */ @Serializable class StaggeredGridLayoutManager_SavedState : Parcelable { + @JvmField var mAnchorPosition = 0 - var mVisibleAnchorPosition // Replacement for span info when spans are invalidated - = 0 + + @JvmField + var mVisibleAnchorPosition = 0 + + @JvmField var mSpanOffsetsSize = 0 + + @JvmField var mSpanOffsets: IntArray? = null + + @JvmField var mSpanLookupSize = 0 + + @JvmField var mSpanLookup: IntArray? = null - var mFullSpanItems: ArrayList? = null + + @JvmField + var mFullSpanItems: List? = null + + @JvmField var mReverseLayout = false + + @JvmField var mAnchorLayoutFromEnd = false + + @JvmField var mLastLayoutRTL = false constructor() @@ -113,13 +131,19 @@ class StaggeredGridLayoutManager_SavedState : Parcelable { */ @Serializable class FullSpanItem : Parcelable { + @JvmField var mPosition = 0 + + @JvmField var mGapDir = 0 + + @JvmField var mGapPerSpan: IntArray? = null // A full span may be laid out in primary direction but may have gaps due to // invalidation of views after it. This is recorded during a reverse scroll and if // view is still on the screen after scroll stops, we have to recalculate layout + @JvmField var mHasUnwantedGapAfter = false constructor(parcel: Parcel) { @@ -165,17 +189,13 @@ class FullSpanItem : Parcelable { + '}') } - companion object { - @JvmField - val CREATOR: Parcelable.Creator = - object : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): FullSpanItem { - return FullSpanItem(parcel) - } + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): FullSpanItem { + return FullSpanItem(parcel) + } - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } } } diff --git a/viewpager2/src/main/res-public/values/public_attrs.xml b/viewpager2/src/main/res-public/values/public_attrs.xml index 22d102b1f..ca0b8e726 100644 --- a/viewpager2/src/main/res-public/values/public_attrs.xml +++ b/viewpager2/src/main/res-public/values/public_attrs.xml @@ -1,4 +1,5 @@ - - - - - - - - - - + + + + + + + + + diff --git a/viewpager2/src/main/res/values/attrs.xml b/viewpager2/src/main/res/values/attrs.xml index e26cdb861..0c81f47b6 100644 --- a/viewpager2/src/main/res/values/attrs.xml +++ b/viewpager2/src/main/res/values/attrs.xml @@ -1,4 +1,19 @@ + + @@ -23,7 +38,7 @@ - + @@ -32,7 +47,4 @@ - - - - + \ No newline at end of file diff --git a/viewpager2/src/main/res/values/dimens.xml b/viewpager2/src/main/res/values/dimens.xml index 5ea5ebd12..bd5cdb4bc 100644 --- a/viewpager2/src/main/res/values/dimens.xml +++ b/viewpager2/src/main/res/values/dimens.xml @@ -1,4 +1,5 @@ - - + \ No newline at end of file