Почему не сгладить прокрутку RecyclerView хорошо работать с некоторыми классами Interpolator?

Фон

Чтобы сделать хороший и краткий обзор элементов на горизонтальном RecyclerView, мы хотим иметь подобную отскоку анимацию, которая начинается с некоторой позиции и переходит к началу RecyclerView (скажем, из пункта 3 в пункт 0),

Эта проблема

По какой-то причине все классы Interpolator, которые я пытаюсь сделать (иллюстрация доступна здесь), похоже, не позволяют элементам выходить за пределы RecyclerView или отказываться от него.

В частности, я пробовал OvershootInterpolator, BounceInterpolator и некоторые другие подобные. Я даже попробовал AnticipateOvershootInterpolator. В большинстве случаев он выполняет простую прокрутку без особого эффекта. на AnticipateOvershootInterpolator, он даже не прокручивает...

Что я пробовал

Вот код POC, который я сделал, чтобы показать проблему:

MainActivity.kt

class MainActivity : AppCompatActivity() {
    val handler = Handler()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val itemSize = resources.getDimensionPixelSize(R.dimen.list_item_size)
        val itemsCount = 6
        recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
                val imageView = ImageView([email protected])
                imageView.setImageResource(android.R.drawable.sym_def_app_icon)
                imageView.layoutParams = RecyclerView.LayoutParams(itemSize, itemSize)
                return object : RecyclerView.ViewHolder(imageView) {}
            }

            override fun getItemCount(): Int = itemsCount

            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            }
        }
        val itemToGoTo = Math.min(3, itemsCount - 1)
        val scrollValue = itemSize * itemToGoTo
        recyclerView.post {
            recyclerView.scrollBy(scrollValue, 0)
            handler.postDelayed({
                recyclerView.smoothScrollBy(-scrollValue, 0, BounceInterpolator())
            }, 500L)
        }
    }
}

activity_main.xml

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView" xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent"
    android:layout_height="@dimen/list_item_size" android:orientation="horizontal"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

файл градиента

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.example.myapplication"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.0.0-rc02'
    implementation 'androidx.core:core-ktx:1.0.0-rc02'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
    implementation 'androidx.recyclerview:recyclerview:1.0.0-rc02'
}

И здесь анимация того, как он выглядит BounceInterpolator, который, как вы можете видеть, не отскакивает вообще:

enter image description here

Пример проекта POC доступен здесь

Вопрос

Почему это не работает должным образом, и как я могу это исправить?

Может ли RecyclerView хорошо работать с Interpolator для прокрутки?


EDIT: кажется, это ошибка, поскольку я не могу использовать какой-либо "интересный" интерполятор для прокрутки RecyclerView, поэтому я сообщил об этом здесь.

Ответ 1

Я бы посмотрел на пакет анимации поддержки Google. В частности, https://developer.android.com/reference/android/support/animation/DynamicAnimation#SCROLL_X

Это будет выглядеть примерно так:

SpringAnimation(recyclerView, DynamicAnimation.SCROLL_X, 0f)
        .setStartVelocity(1000)
        .start()

ОБНОВИТЬ:

Похоже, это тоже не работает. Я посмотрел на некоторые источники для RecyclerView и причина, по которой интерполятор отказов не работает, потому что RecyclerView неправильно использует интерполятор. Там вызов computeScrollDuration вызывает интерполятор, а затем получает необработанное время анимации в секундах вместо значения как% от общего времени анимации. Это значение также не вполне предсказуемо, я проверил несколько значений и увидел где-то от 100 мс до 250 мс. В любом случае, из того, что я вижу, у вас есть два варианта (я тестировал оба)

  1. Пользователь другой библиотеки, такой как https://github.com/EverythingMe/overscroll-decor

  2. Внесите свою собственную собственность и используйте анимацию весны:


class ScrollXProperty : FloatPropertyCompat("scrollX") {

    override fun setValue(obj: RecyclerView, value: Float) {
        obj.scrollBy(value.roundToInt() - getValue(obj).roundToInt(), 0)
    }

    override fun getValue(obj: RecyclerView): Float =
            obj.computeHorizontalScrollOffset().toFloat()
}

Затем в своем отказе замените вызов smoothScrollBy с изменением:

SpringAnimation(recyclerView, ScrollXProperty())
    .setSpring(SpringForce()
            .setFinalPosition(0f)
            .setStiffness(SpringForce.STIFFNESS_LOW)
            .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY))
    .start()

ОБНОВИТЬ:

Второе решение работает вне коробки, без изменений в вашем RecyclerView, и это тот, который я написал и протестировал полностью.

Подробнее о интерполяторах, smoothScrollBy не работает с интерполяторами (вероятно, ошибка). При использовании интерполятора вы в основном сопоставляете значение 0-1 с другим, что является мультипликатором для анимации. Пример: t = 0, interp (0) = 0 означает, что в начале анимации значение должно быть таким же, как оно было начато, t =.5, interp (.5) =. 25 означает, что элемент будет анимировать 1/4 пути и т.д. Интерполяторы отскока в основном возвращают значения> 1 в какой-то момент и колеблются около 1 до окончательного осаждения при 1 при t = 1.

Какое решение № 2 используется с использованием весеннего аниматора, но для обновления прокрутки. Причина, по которой SCROLL_X не работает, заключается в том, что RecyclerView фактически не прокручивается (это была моя ошибка). Он вычисляет, где представления должны быть основаны на другом расчете, поэтому вам нужен вызов для computeHorizontalScrollOffset. ScrollXProperty позволяет вам изменять горизонтальную прокрутку RecyclerView, как если бы вы scrollX свойство scrollX в ScrollView, это в основном адаптер. RecyclerViews не поддерживают прокрутку к определенному смещению пикселей, только при плавной прокрутке, но SpringAnimation уже делает это плавно для вас, поэтому нам это не нужно. Вместо этого мы хотим перейти к отдельной позиции. См. Https://android.googlesource.com/platform/frameworks/support/+/247185b98675b09c5e98c87448dd24aef4dffc9d/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java#387

ОБНОВИТЬ:

Здесь код, который я использовал для проверки https://github.com/yperess/StackOverflow/tree/52148251

ОБНОВИТЬ:

Получил ту же концепцию, которая работает с интерполяторами:

class ScrollXProperty : Property<RecyclerView, Int>(Int::class.java, "horozontalOffset") {
    override fun get('object': RecyclerView): Int =
            'object'.computeHorizontalScrollOffset()

    override fun set('object': RecyclerView, value: Int) {
        'object'.scrollBy(value - get('object'), 0)
    }
}

ObjectAnimator.ofInt(recycler_view, ScrollXProperty(), 0).apply {
    interpolator = BounceInterpolator()
    duration = 500L
}.start()

Обновлен демонстрационный проект GitHub

Я обновил ScrollXProperty чтобы включить оптимизацию, кажется, хорошо работает на моем Pixel, но я не тестировал старые устройства.

class ScrollXProperty(
        private val enableOptimizations: Boolean
) : Property<RecyclerView, Int>(Int::class.java, "horizontalOffset") {

    private var lastKnownValue: Int? = null

    override fun get('object': RecyclerView): Int =
            'object'.computeHorizontalScrollOffset().also {
                if (enableOptimizations) {
                    lastKnownValue = it
                }
            }

    override fun set('object': RecyclerView, value: Int) {
        val currentValue = lastKnownValue?.takeIf { enableOptimizations } ?: get('object')
        if (enableOptimizations) {
            lastKnownValue = value
        }
        'object'.scrollBy(value - currentValue, 0)
    }
}

Проект GitHub теперь включает демонстрацию со следующими интерполяторами:

<string-array name="interpolators">
    <item>AccelerateDecelerate</item>
    <item>Accelerate</item>
    <item>Anticipate</item>
    <item>AnticipateOvershoot</item>
    <item>Bounce</item>
    <item>Cycle</item>
    <item>Decelerate</item>
    <item>Linear</item>
    <item>Overshoot</item>
</string-array>

Ответ 2

Как я уже сказал, я бы честно не ожидал, что кандидат на выпуск (recyclerview:1.0.0-rc02 в вашем случае) будет работать нормально, не вызывая никаких проблем, поскольку он уже находится в разработке.

  • Использование сторонних библиотек может работать, но на самом деле я так не думаю, так как они только что представили зависимости от androidx и они уже находятся в разработке и недостаточно стабильны для использования другими разработчиками.

Обновление Ответ от разработчиков Google:

Мы передали это команде разработчиков и обновим эту проблему с дополнительной информацией по мере ее появления.

Итак, лучше ждать новых обновлений.

Ответ 3

Вам нужно включить откат прокрутки.

Одним из возможных решений является использование https://github.com/chthai64/overscroll-bouncy-android

Включите его в свой проект

implementation 'com.chauthai.overscroll:overscroll-bouncy:0.1.1'

А затем в вашем POV измените RecyclerView в activity_main.xml на com.chauthai.overscroll.RecyclerViewBouncy

<?xml version="1.0" encoding="utf-8"?>
<com.chauthai.overscroll.RecyclerViewBouncy
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="@dimen/list_item_size"
    android:orientation="horizontal"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

После этого изменения вы увидите отскок в своем приложении.