It's my first question in this incredible community.
I'm developing an android app in kotlin. I need a permanent bottomsheet (not modal). I have developed all the behavior that I needed, but for one detail.
I need to set de STATE_HALF_EXPANDED, by default is 50% of the screen, but I need 70%. I have visited this question:
How i can set Half Expanded state for my BottomSheet
In that question, the user Adauton Heringer explain how to do it in one of the answers. He said:
behavior = BottomSheetBehavior.from(your_bottom_sheet_xml)
behavior.isFitToContents = false
behavior.halfExpandedRatio = 0.6f
I tried the same, because it looks like very easy to do. I did the two first lines, but when I try to used setHalfExpandedRatio() is like it wasn't exist. I have checked the official documentation and it is a public method.
https://developer.android.com/reference/com/google/android/material/bottomsheet/BottomSheetBehavior#sethalfexpandedratio
Am I doing something wrong?
My code is this:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var bottomSheet: View = view.findViewById(R.id.departures_bottomsheet)
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
bottomSheetBehavior.isFitToContents = false
// this doesn't work for me
// bottomSheetBehavior.halfExpandedRatio = 0.7
bottomSheetBehavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(p0: View, dragPoint: Float) {
val upper = 0.66
val lower = 0.33
if (dragPoint >= upper) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
if (dragPoint < upper && dragPoint >= lower) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
}
if (dragPoint < lower) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
override fun onStateChanged(p0: View, p1: Int) {
}
} )
}
I have this import:
import com.google.android.material.bottomsheet.BottomSheetBehavior
And this implementation in build.gradle of the app:
implementation 'com.google.android.material:material:1.0.0'
In the layout file, the View is a child of a CoordinatorLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/globalMap"
class="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.circularreveal.CircularRevealLinearLayout
android:id="#+id/departures_bottomsheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/white"
app:behavior_peekHeight="80dp"
app:layout_behavior="#string/bottom_sheet_behavior">
I have navigated to BottomSheetBehavior.class in Android Studio and this method doesn't exist.
Any help is welcome and I will be grateful.
If I don't find any other way, I will create a SubClass with this method.
From BottomSheetBehavior.java commit history, the method setHalfExpandedRatio(float ratio) is added from version 1.1.0-alpha05.
You are using vevrsion 1.0.0, that why you cannot see this method.
Solution: Change version code from 1.0.0 to 1.1.0-alpha05 in your gradle file.
// implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.android.material:material:1.1.0-alpha05'
or using the latest version
// implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.android.material:material:1.2.0-alpha03'
You can find all available versions here.
Related
I have an android module (ComposeLib) as part of the same project as the app. It's just to test out using Compose from a library. In the Project Structure dialog I added ComposeLib as an implementation dependency of app.
build.gradle (:app) contains...
dependencies {
...
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.4.0-rc01'
implementation "com.google.accompanist:accompanist-appcompat-theme:0.16.0"
implementation project(path: ':ComposeLib')
...
}
Atoms.kt in ComposeLib consists of...
class Atoms {
#Composable
fun CounterButton(count: Int, updateCount: (Int) -> Unit) {
Button( onClick = {updateCount(count+1)},
modifier = Modifier
.background(MaterialTheme.colors.secondary)){
Text("Clicked $count times")
}
}
}
Then in MainActivity.kt I am trying to use CounterButton...
import com.example.composelib.Atoms
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val myComposeView = findViewById<ComposeView>(R.id.composeView)
myComposeView.setContent {
val counter = remember{ mutableStateOf(0) }
AppCompatTheme {
CounterButton( // <== Unresolved Reference!?
count = counter.value,
updateCount = {newCount -> counter.value = newCount})
}
}
}
}
As you can see in the lower left of the screenshot the app cant find CounterButton from ComposeLib.Atoms. Any idea why?
This code works if I put CounterButton() in the app in MainActivity, so it's not a Jetpack problem it's a build configuration problem.
I also tried qualifying the call to CounterButton every way I could think of (Atoms.CounterButton, public.example.composelib.Atoms.CounterButton, etc). Even code completion doesn't recognize it.
How do I reference a #Composable function from another module in the same project?
You've defined your Composable inside class Atoms for some reason, so this function should be called on a class instance.
It's totally fine to define composable functions without any classes, just like
#Composable
fun CounterButton(count: Int, updateCount: (Int) -> Unit) {
}
It's already in some package so I don't think any container is much needed. But in case you wanna add some kind of modularity, you can replace class with object, in this case you'll be able to call it as Atoms.CounterButton
We are having hard times to smoothly resize a here SDK map on Android.
We want to smoothly resize the map to the bottom sheet collapse and hidden state as shown in
But as you can see it does not really resize instead its jumps to the new position while the map keeps its dimensions and does not scale.
And this is what we did:
...
<com.here.sdk.mapview.MapView
android:id="#+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="#dimen/nine_grid_unit" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/menuBottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/white"
android:clickable="true"
android:elevation="#dimen/four_grid_unit"
android:focusable="true"
app:behavior_hideable="true"
app:behavior_peekHeight="#dimen/thirtytwo_grid_unit"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<View
android:id="#+id/tap_stop"
android:layout_width="#dimen/nine_grid_unit"
android:layout_height="#dimen/one_grid_unit"
android:layout_marginTop="#dimen/one_grid_unit"
android:background="#color/grey_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<edeka.digital.app.widget.SegmentedControlView
android:id="#+id/tabSwitchSegmentedControl"
android:layout_width="#dimen/thirtyfive_grid_unit"
android:layout_height="wrap_content"
android:paddingStart="#dimen/three_grid_unit"
android:paddingEnd="#dimen/three_grid_unit"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/tap_stop"
app:segmentCount="2"
app:segmentTitles="#array/segment_titles_shop_search" />
</androidx.constraintlayout.widget.ConstraintLayout>
...
And code:
val bottomBehavior = BottomSheetBehavior.from(binding.menuBottomSheet)
bottomBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
val mapView = binding.map
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
bottomSheetBehaviorObservable.onNext(newState)
when (newState) {
BottomSheetBehavior.STATE_COLLAPSED -> {
mapView.bottom = binding.menuBottomSheet.top
mapView.invalidate()
}
BottomSheetBehavior.STATE_HIDDEN -> {
mapView.bottom = binding.menuBottomSheet.top
mapView.invalidate()
}
else -> { /* void */
}
}
}
})
I would have expected some kind of resize() function or that it layouts itself if layout dimensions change.
What we really want is already implemented in HERE WeGo App. The whole maps scales (inc. here logo) if user swipes the bottom sheet:
Can anyone help us out?
The demo shown in 1 can be found here:
https://github.com/edekadigital/heremaps-demo
The best solution that I've found to achieve it is to add a new method:
private fun updateMapView(bottomSheetTop: Int) {
val mapView = binding.map
val principalY = Math.min(bottomSheetTop / 2.0, mapView.height / 2.0)
mapView.camera.principalPoint = Point2D(mapView.width / 2.0, principalY)
val logoMargin = Math.max(0, mapView.bottom - bottomSheetTop)
mapView.setWatermarkPosition(WatermarkPlacement.BOTTOM_CENTER, logoMargin.toLong())
}
and call it in onSlide and onStateChanged like this:
updateMapView(bottomSheet.top)
Note that you need to have the HERE logo at the bottom center position, otherwise it can't use an adjustable margin.
I was also trying to resize the map view, but the results were unsatisfying. Here is the code if you want to give a try:
private fun updateMapView(bottomSheetTop: Int) {
val mapView = binding.map
mapView.layoutParams.height = bottomSheetTop
mapView.requestLayout()
}
It looks like that your map view is covered by the sliding panel and is not redrawn during slide animation. It renders only when the state changes. You can try to add mapView.invalidate() in onSlide method, like this:
override fun onSlide(bottomSheet: View, slideOffset: Float) {
mapView.invalidate()
}
However, to be sure if that's the actual reason, I would need to get and build your code.
I was able to get your code, compile and reproduce the bug. I've found two options to fix that, both tested on an emulator and a real device.
Copy the code from state change handling code into onSlide method:
override fun onSlide(bottomSheet: View, slideOffset: Float) {
mapView.bottom = binding.menuBottomSheet.top
mapView.invalidate()
}
Remove map view resizing and invalidating code at all. It basically makes the whole setupBottomSheet method redundant. Map view works correctly without resizing and it's a preferable way to fix it, as it involves less code and operations.
I have a MotionLayout inside the NestedScrollView:
<androidx.core.widget.NestedScrollView
android:id="#+id/scroll_content"
android:layout_width="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="#+id/content_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
app:layoutDescription="#xml/main_scene">
<View 1>
<View 2>
<View 3>
</androidx.constraintlayout.motion.widget.MotionLayout>
My state 1 shows View 1 only.
My state 2 shows View 2 only.
My state 3 shows View 1 + View 2(below View 1) + View 3(below View 2)
Since state 3 appends multiple views vertically, it is the longest vertically.
However, I can only scroll down to the amount set for state 1 & state 2. It does not reset the height inside the scrollView.
Am I doing something wrong?
I tried following at onTransitionCompleted():
scroll_content.getChildAt(0).invalidate()
scroll_content.getChildAt(0).requestLayout()
scroll_content.invalidate()
scroll_content.requestLayout()
They did not solve my issue.
Adding motion:layoutDuringTransition="honorRequest" inside the <Transition> in your layoutDescription XML file fixes the issue.
This was added to ConstraintLayout in version 2.0.0-beta4
Unfortunately, I also encountered such a problem, but found a workaround
<ScrollView>
<LinearLayout>
<MotionLayout>
and after the animation is completed
override fun onTransitionCompleted(contentContainer: MotionLayout?, p1: Int) {
val field = contentContainer::class.java.getDeclaredField("mEndWrapHeight")
field.isAccessible = true
val newHeight = field.getInt(contentContainer)
contentContainer.requestNewSize(contentContainer.width, newHeight)
}
requestViewSize this is an extension function
internal fun View.requestNewSize(width: Int, height: Int) {
layoutParams.width = width
layoutParams.height = height
layoutParams = layoutParams
}
if you twitch when changing height, just add animateLayoutChanges into your main container and your MotionLayout.
Add if necessary in code
yourView.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
--------UPDATE--------
I think I found a more correct option for animating the change in height.
Just the first line in the method onTransitionEnd, insert scroll.fullScroll (ScrollView.FOCUS_UP). I added so that the code for changing the height is executed in 500 milliseconds
override fun onTransitionCompleted(contentContainer: MotionLayout?, currentState: Int) {
if (currentState == R.id.second_state) {
scroll.fullScroll(ScrollView.FOCUS_UP)
GlobalScope.doAfterDelay(500) {
if (currentState == R.id.second_state) {
val field = contentContainer::class.java.getDeclaredField("mEndWrapHeight")
field.isAccessible = true
val newHeight = field.getInt(contentContainer)
contentContainer.requestNewSize(contentContainer.width, newHeight)
}
}
}
}
doAfterDelay is a function extension for coroutine
fun GlobalScope.doAfterDelay(time: Long, code: () -> Unit) {
launch {
delay(time)
launch(Dispatchers.Main) { code() }
}
}
But you can use alitenative
I would like to calculate the navigationBar height. I've seen this presentation : https://chris.banes.me/talks/2017/becoming-a-master-window-fitter-nyc/
So, I tried to use the method View.setOnApplyWindowInsetsListener().
But, for some reason, it's never called.
Does anyone knows why ? Any limitation there ?
I've tried to use it like this :
navBarOverlay.setOnApplyWindowInsetsListener { v, insets ->
Timber.i("BOTTOM = ${insets.systemWindowInsetBottom}")
return#setOnApplyWindowInsetsListener insets
}
Note that my root layout is a ConstraintLayout.
I faced the same issue.
If your root view is ConstraintLayout and contains android:fitsSystemWindows="true" attr, the view consumed onApplyWindowInsets callbacks.
So if you set onApplyWindowInsets on child views, they never get onApplyWindowInsets callbacks.
Or check your parent views consume the callback.
This is what I observed; in other words, your experience might be different.
[Layout for Activity]
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true" <--
tools:context=".MyAppActivity">
...
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="#dimen/fab_margin" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Notice android:fitsSystemWindows="true" in the outer most layout. As long as we have it, setOnApplyWindowInsetsListener() does get called.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
ViewCompat.setOnApplyWindowInsetsListener(fab) { view, insets ->
...
insets
}
}
Alternatively, if you are going for the "full screen", meaning you want your layout to extend to the status bar and the navigation bar, you can do something like the following.
override fun onCreate(savedInstanceState: Bundle?) {
...
WindowCompat.setDecorFitsSystemWindows(window, false) <--
ViewCompat.setOnApplyWindowInsetsListener(fab) { view, insets ->
...
insets
}
}
The same idea is applicable when you are using a Fragment, as long as the Activity (that contains the Fragment) has either fitsSystemWindows in the outer most layout or you set your Activity as full screen.
I have faced with this problem when I've used CollapsingToolbarLayout, problem is that CollapsingToolbarLayout not invoking insets listener, if you have CollapsingToolbarLayout, then right after this component all other view insets wouldn't be triggered. If so, then remove listener from CollapsingToolbarLayout by calling
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout, null)
If you don't CollapsingToolbarLayout, then some other view is blocking insets from passing from view to view.
Or you have already consumed them, I guess you didn't do it)
There is also bug with CollapsingToolbarLayout, it prevents siblings to receive insets, you can see it in issues github link. One of the solutions is to putAppbarLayout below in xml other views for them to receive insets.
I've had to (and I think I am expected to) explicitly call requestApplyInsets() at some appropriate time to make the listener get hit.
Check this article for some possible tips: https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
My solution is to call it on navBarOverlay.rootView.
putting ViewCompat.setOnApplyWindowInsetsListener into onResume worked for me with constraintLayout.
#Override
public void onResume() {
super.onResume();
ViewCompat.setOnApplyWindowInsetsListener(requireActivity().getWindow().getDecorView(), (v, insets) -> {
boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
return insets;
});
}
I faced similar issue on API 30.
For setOnApplyWindowInsetsListener() to work you have to make sure that your activity is in full-screen mode. You can use below method to do so
WindowCompat.setDecorFitsSystemWindows(activity.window, false) //this is backward compatible version
Also make sure you are not using below method anywhere to set UI flags
View.setSystemUiVisibility(int visibility)
I had this problem on android 7.1. But on android 11 it worked correctly. Just create a class:
import android.content.Context
import android.util.AttributeSet
import androidx.core.view.ViewCompat
import com.google.android.material.appbar.CollapsingToolbarLayout
class InsetsCollapsingToolbarLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : CollapsingToolbarLayout(context, attrs, defStyle) {
init {
ViewCompat.setOnApplyWindowInsetsListener(this, null)
}
}
And use everywhere InsetsCollapsingToolbarLayout instead of CollapsingToolbarLayout
In my app, it gets called once and not every time I wanted to. Therefore, in that one time it gets called, I saved the widnowInsets to a global variable to use it throughout the app.
I used the following solution using this answer:
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(android.R.id.content)
) { _: View?, insets: WindowInsetsCompat ->
navigationBarHeight = insets.systemWindowInsetBottom
insets
}
I used following solution in my project and it's works like a charm.
val decorView: View = requireActivity().window.decorView
val rootView = decorView.findViewById<View>(android.R.id.content) as ViewGroup
ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
val isKeyboardVisible = isKeyboardVisible(insets)
Timber.d("isKeyboardVisible: $isKeyboardVisible")
// Do something with isKeyboardVisible
insets
}
private fun isKeyboardVisible(insets: WindowInsetsCompat): Boolean {
val systemWindow = insets.systemWindowInsets
val rootStable = insets.stableInsets
if (systemWindow.bottom > rootStable.bottom) {
// This handles the adjustResize case on < API 30, since
// systemWindow.bottom is probably going to be the IME
return true
}
return false
}
Use setWindowInsetsAnimationCallback instead of setOnApplyWindowInsetsListener in Android API > 30
One problem I had is ViewCompat.setOnApplyWindowInsetsListener was called again in some other place in the code for the same view. So make sure that is only set once.
Background
In an effort to make a nice&short overview of the items on a horizontal RecyclerView, we want to have a bounce-like animation , that starts from some position, and goes to the beginning of the RecyclerView (say, from item 3 to item 0) .
The problem
For some reason, all Interpolator classes I try (illustration available here) don't seem to allow items to go outside of the RecyclerView or bounce on it.
More specifically, I've tried OvershootInterpolator , BounceInterpolator and some other similar ones. I even tried AnticipateOvershootInterpolator. In most cases, it does a simple scrolling, without the special effect. on AnticipateOvershootInterpolator , it doesn't even scroll...
What I've tried
Here's the code of the POC I've made, to show the issue:
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(this#MainActivity)
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"/>
gradle file
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'
}
And here's an animation of how it looks for BounceInterpolator , which as you can see doesn't bounce at all :
Sample POC project available here
The question
Why doesn't it work as expected, and how can I fix it?
Could RecyclerView work well with Interpolator for scrolling ?
EDIT: seems it's a bug, as I can't use any "interesting" interpolator for RecyclerView scrolling, so I've reported about it here .
I would take a look at Google's support animation package. Specifically https://developer.android.com/reference/android/support/animation/DynamicAnimation#SCROLL_X
It would look something like:
SpringAnimation(recyclerView, DynamicAnimation.SCROLL_X, 0f)
.setStartVelocity(1000)
.start()
UPDATE:
Looks like this doesn't work either. I looked at some of the source for RecyclerView and the reason that the bounce interpolator doesn't work is because RecyclerView isn't using the interpolator correctly. There's a call to computeScrollDuration the calls to the interpolator then get the raw animation time in seconds instead of the value as a % of the total animation time. This value is also not entirely predictable I tested a few values and saw anywhere from 100ms - 250ms. Anyway, from what I'm seeing you have two options (I've tested both)
User another library such as https://github.com/EverythingMe/overscroll-decor
Implement your own property and use the spring animation:
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()
}
Then in your bounce, replace the call to smoothScrollBy with a variation of:
SpringAnimation(recyclerView, ScrollXProperty())
.setSpring(SpringForce()
.setFinalPosition(0f)
.setStiffness(SpringForce.STIFFNESS_LOW)
.setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY))
.start()
UPDATE:
The second solution works out-of-box with no changes to your RecyclerView and is the one I wrote and tested fully.
More about interpolators, smoothScrollBy doesn't work well with interpolators (likely a bug). When using an interpolator you basically map a 0-1 value to another which is a multiplier for the animation. Example: t=0, interp(0)=0 means that at the start of the animation the value should be the same as it started, t=.5, interp(.5)=.25 means that the element would animate 1/4 of the way, etc. Bounce interpolators basically return values > 1 at some point and oscillate about 1 until finally settling at 1 when t=1.
What solution #2 is doing is using the spring animator but needing to update scroll. The reason SCROLL_X doesn't work is that RecyclerView doesn't actually scroll (that was my mistake). It calculates where the views should be based on a different calculation which is why you need the call to computeHorizontalScrollOffset. The ScrollXProperty allows you to change the horizontal scroll of a RecyclerView as though you were specifying the scrollX property in a ScrollView, it's basically an adapter. RecyclerViews don't support scrolling to a specific pixel offset, only in smooth scrolling, but the SpringAnimation already does it smoothly for you so we don't need that. Instead we want to scroll to a discrete position. See https://android.googlesource.com/platform/frameworks/support/+/247185b98675b09c5e98c87448dd24aef4dffc9d/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java#387
UPDATE:
Here's the code I used to test https://github.com/yperess/StackOverflow/tree/52148251
UPDATE:
Got the same concept working with interpolators:
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()
Demo project on GitHub was updated
I updated ScrollXProperty to include an optimization, it seems to work well on my Pixel but I haven't tested on older devices.
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)
}
}
The GitHub project now includes demo with the following interpolators:
<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>
Like I said, I wouldn't honestly expect a Release Candidate (recyclerview:1.0.0-rc02 in your case) would work properly without causing any issues since it's already under development.
Using third-party libraries might work but, I don't really think so since they have just introduced androidx dependencies and they're already under development and not stable enough to use by other developers.
Update The answer from Google Developers:
We have passed this to the development team and will update this issue
with more information as it becomes available.
So, better to wait for the new updates.
You need to enable overscroll bounce somehow.
The one of the possible solutions is to use
https://github.com/chthai64/overscroll-bouncy-android
Include it in your project
implementation 'com.chauthai.overscroll:overscroll-bouncy:0.1.1'
And then in your POV change RecyclerView in activity_main.xml to 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"/>
After this change you will see bounce in your app.