I have a RecyclerView holding a list of Artists. An artist can be marked as a favorite or not a favorite.
There's a heart to represent this state, and I have a transition drawable, where I'm going from a outlined heart, to a filled in heart based on if the artist in question has been marked as a favorite. The animation itself is working correctly and smoothly transitions between the two when the user clicks on the heart, and looks beautiful.
TransitionDrawable td = new TransitionDrawable(new Drawable[]{
ContextCompat.getDrawable(getActivity(), R.drawable.favorite_border),
ContextCompat.getDrawable(getActivity(), R.drawable.favorite)});
artistViewHolder.image.setImageDrawable(td);
if (artist.isFavorite()) {
td.startTransition(0);
}
In the onClick for the item, I call either .reverseTransition(300) or .startTransition(300) depending on the next state.
I'm running into problems anytime the view is created (1st screen load) and I need to start in the 2nd position (favorite). The initial load you can see it flicker from the empty heart to the filled heart, even though my animation time is set to 0 when it's created. This also happens anytime the list gets invalidated such as if it gets filtered into a smaller set of artists.
I guess since it needs to do things and isn't actually just setting the drawable, 0 isn't actually "instant". I can't find any other way to start in the "end" position though, other than to reverse the starting position of the images in the transition drawable itself.
If I do that though, start/reverse are going to behave differently on a heart by heart basis. At best, I would need to extend the transition drawable, and override the start/reverse/reset methods to take into account which initial state it's in, and that seems kind of hacky?
Am I missing something obvious to start in the end position but not cause that flicker?
Thanks!
You don't migtht remove flicker on td.startTransition(0);
So, you need change order in your Drawables array.
For example
Drawable favorite_border = ContextCompat.getDrawable(getActivity(), R.drawable.favorite_border);
Drawable favorite = ContextCompat.getDrawable(getActivity(), R.drawable.favorite);
TransitionDrawable td = new TransitionDrawable(isFavorite
? new Drawable[]{favorite, favorite_border}
: new Drawable[]{favorite_border, favorite});
artistViewHolder.image.setImageDrawable(td);
This will work provided that you do this in onBindViewHolder
Unless there's a better way to do this, I've gone ahead and extended it, and flipped the start/reverse call based off the initial state.
public class MyTransitionDrawable extends TransitionDrawable{
private boolean initialFavorite = false;
public MyTransitionDrawable(Drawable[] layers) {
super(layers);
}
public MyTransitionDrawable(Drawable[] layers, boolean initialFavorite) {
super(layers);
this.initialFavorite = initialFavorite;
}
public boolean isInitialFavorite() {
return initialFavorite;
}
#Override
public void startTransition(int durationMillis) {
if(!initialFavorite){
super.startTransition(durationMillis);
}else{
super.reverseTransition(durationMillis);
}
}
#Override
public void reverseTransition(int durationMillis) {
if(!initialFavorite){
super.reverseTransition(durationMillis);
}else{
super.startTransition(durationMillis);
}
}
}
`
I have implemented a horizontal scrollable RecyclerView. My RecyclerView uses a LinearLayoutManager, and the problem I am facing is that when I try to use scrollToPosition(position) or smoothScrollToPosition(position) or from LinearLayoutManager's scrollToPositionWithOffset(position). Neither works for me. Either a scroll call doesn't scroll to the desired location or it doesn't invoke the OnScrollListener.
So far I have tried so many different combinations of code that I cannot post them all here. Following is the one that works for me (But only partially):
public void smoothUserScrollTo(final int position) {
if (position < 0 || position > getAdapter().getItemCount()) {
Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
return;
}
if (getLayoutManager() == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
"Call setLayoutManager with a non-null layout.");
return;
}
if (getChildAdapterPosition(getCenterView()) == position) {
return;
}
stopScroll();
scrollToPosition(position);
if (lastScrollPosition == position) {
addOnLayoutChangeListener(new OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (left == oldLeft && right == oldRight && top == oldTop && bottom == oldBottom) {
removeOnLayoutChangeListener(this);
updateViews();
// removing the following line causes a position - 3 effect.
scrollToView(getChildAt(0));
}
}
});
}
lastScrollPosition = position;
}
#Override
public void scrollToPosition(int position) {
if (position < 0 || position > getAdapter().getItemCount()) {
Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
return;
}
if (getLayoutManager() == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
"Call setLayoutManager with a non-null layout.");
return;
}
// stopScroll();
((LinearLayoutManager) getLayoutManager()).scrollToPositionWithOffset(position, 0);
// getLayoutManager().scrollToPosition(position);
}
I opted for scrollToPositionWithOffset() because of this but the case perhaps is different as I use a LinearLayoutManager instead of GridLayoutManager. But the solution does work for me too, but as I said earlier only partially.
When the call to scroll is from 0th position to totalSize - 7 scroll works like a charm.
When scroll is from totalSize - 7 to totalSize - 3, First time I only scroll to 7th last item in the list. The second time however I can scroll fine
When scrolling from totalSize - 3 to totalSize, I start getting unexpected behavior.
If anyone has found a work around I'd Appreciate it. Here's the gist to my code of custom ReyclerView.
I had the same issue some weeks ago, and found only a really bad solution to solve it. Had to use a postDelayed with 200-300ms.
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
yourList.scrollToPosition(position);
}
}, 200);
If you found a better solution, please let me know! Good luck!
Turns out I was having a similar issue until I utilized
myRecyclerview.scrollToPosition(objectlist.size()-1)
It would always stay at the top when only putting in the objectlist size. This was until i decided to set the size equal to a variable. Again, that didn't work. Then I assumed that perhaps it was handling an outofboundsexception without telling me. So I subtracted it by 1. Then it worked.
The accepted answer will work, but it may also break. The main reason for this issue is that the recycler view may not be ready by the time you ask it to scroll. The best solution for the same is to wait for the recycler view to be ready and then scroll. Luckily android has provided one such option. Below solution is for Kotlin, you can try the java alternative for the same, it will work.
newsRecyclerView.post {
layoutManager?.scrollToPosition(viewModel.selectedItemPosition)
}
The post runnable method is available for every View elements and will execute once the view is ready, hence ensuring the code is executed exactly when required.
You can use LinearSmoothScroller this worked every time in my case:
First create an instance of LinearSmoothScroller:
LinearSmoothScroller smoothScroller=new LinearSmoothScroller(activity){
#Override
protected int getVerticalSnapPreference() {
return LinearSmoothScroller.SNAP_TO_START;
}
};
And then when you want to scroll recycler view to any position do this:
smoothScroller.setTargetPosition(pos); // pos on which item you want to scroll recycler view
recyclerView.getLayoutManager().startSmoothScroll(smoothScroller);
Done.
So the problem for me was that I had a RecyclerView in a NestedScrollView. Took me some time to figure out this was the problem. The solution for this is (Kotlin):
val childY = recycler_view.y + recycler_view.getChildAt(position).y
nested_scrollview.smoothScrollTo(0, childY.toInt())
Java (credits to Himagi https://stackoverflow.com/a/50367883/2917564)
float y = recyclerView.getY() + recyclerView.getChildAt(selectedPosition).getY();
scrollView.smoothScrollTo(0, (int) y);
The trick is to scroll the nested scrollview to the Y instead of the RecyclerView. This works decently at Android 5.0 Samsung J5 and Huawei P30 pro with Android 9.
I also faced a similar problem (having to scroll to the top when the list is getting updated), but none of the above options worked 100%
However I finally found a working solution at https://dev.to/aldok/how-to-scroll-recyclerview-to-a-certain-position-5ck4 archive link
Summary
scrollToPosition only seems to work when the underlying dataset is ready.
So therefore postDelay works (randomly) but it's depending on the speed of the device/app. If the timeout is too short it fails. smoothScrollToPosition also only works if the adapter is not too busy (see https://stackoverflow.com/a/61403576/11649486)
To observe when the dataset is ready, a AdapterDataObserver can be added and certain methods overridden.
The code that fixed my problem:
adapter.registerAdapterDataObserver( object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(
positionStart: Int,
itemCount: Int
) {
// This will scroll to the top when new data was inserted
recyclerView.scrollToPosition(0)
}
}
None of the methods seems to be working for me. Only the below single line of code worked
((LinearLayoutManager)mRecyclerView.getLayoutManager()).scrollToPositionWithOffset(adapter.currentPosition(),200);
The second parameter refers to offset, which is actually the distance (in pixels) between the start edge of the item view and start edge of the RecyclerView. I have supplied it with a constant value to make the top items also visible.
Check for more reference over here
Using Kotlin Coroutines in Fragment or Activity, and also using the lifecycleScope since any coroutine launched in this scope is canceled when the Lifecycle is destroyed.
lifecycleScope.launch {
delay(100)
recyclerView.scrollToPosition(0)
This worked for me
Handler().postDelayed({
(recyclerView.getLayoutManager() as LinearLayoutManager).scrollToPositionWithOffset( 0, 0)
}, 100)
I had the same issue while creating a cyclic/circular adapter, where I could only scroll downward but not upward considering the position initialises to 0. I first considered using Robert's approach, but it was too unreliable as the Handler only fired once, and if I was unlucky the position wouldn't get initialised in some cases.
To resolve this, I create an interval Observable that checks every XXX amount of time to see whether the initialisation succeeded and afterward disposes of it. This approach worked very reliably for my use case.
private fun initialisePositionToAllowBidirectionalScrolling(layoutManager: LinearLayoutManager, realItemCount: Int) {
val compositeDisposable = CompositeDisposable() // Added here for clarity, make this into a private global variable and clear in onDetach()/onPause() in case auto-disposal wouldn't ever occur here
val initPosition = realItemCount * 1000
Observable.interval(INIT_DELAY_MS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe ({
if (layoutManager.findFirstVisibleItemPosition() == 0) {
layoutManager.scrollToPositionWithOffset(initPosition, 0)
if (layoutManager.findFirstCompletelyVisibleItemPosition() == initPosition) {
Timber.d("Adapter initialised, setting position to $initPosition and disposing interval subscription!")
compositeDisposable.clear()
}
}
}, {
Timber.e("Failed to initialise position!\n$it")
compositeDisposable.clear()
}).let { compositeDisposable.add(it) }
}
This worked perfectly for when scrolling to last item in the recycler
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
if (((LinearLayoutManager) recyclerView.getLayoutManager())
.findLastVisibleItemPosition() != adapter.getItemCount() - 1) {
recyclerView.scrollToPosition(adapter.getItemCount() - 1);
handler.postDelayed(this, 200);
}
}
}, 200 /* change it if you want*/);
Pretty weird bug, anyway I managed to work around it without post or post delayed as follow:
list.scrollToPosition(position - 1)
list.smoothScrollBy(1, 0)
Hopefully, it helps someone too.
Had the same issue. My problem was, that I refilled the view with data in an async task, after I tried to scroll. From onPostExecute ofc fixed this problem. A Delay fixed this issue too, because when the scroll executed, the list had already been refilled.
I use below solution to make the selected item in recycler view visible after the recycler view is reloaded (orientation change, etc). It overrides LinearLayoutManager and uses onSaveInstanceState to save current recycler position. Then in onRestoreInstanceState the saved position is restored. Finaly, in onLayoutCompleted, scrollToPosition(mRecyclerPosition) is used to make the previously selected recycler position visible again, but as Robert Banyai stated, for it to work reliably a certain delay must be inserted. I guess it is needed to provide enough time for adapter to load the data before scrollToPosition is called.
private class MyLayoutManager extends LinearLayoutManager{
private boolean isRestored;
public MyLayoutManager(Context context) {
super(context);
}
public MyLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public MyLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
if(isRestored && mRecyclerPosition >-1) {
Handler handler=new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
MyLayoutManager.this.scrollToPosition(mRecyclerPosition);
}
},200);
}
isRestored=false;
}
#Override
public Parcelable onSaveInstanceState() {
Parcelable savedInstanceState = super.onSaveInstanceState();
Bundle bundle=new Bundle();
bundle.putParcelable("saved_state",savedInstanceState);
bundle.putInt("position", mRecyclerPosition);
return bundle;
}
#Override
public void onRestoreInstanceState(Parcelable state) {
Parcelable savedState = ((Bundle)state).getParcelable("saved_state");
mRecyclerPosition = ((Bundle)state).getInt("position",-1);
isRestored=true;
super.onRestoreInstanceState(savedState);
}
}
If you use recyclerview in nestedScrollView you must scroll nestScrollview
nestedScrollview.smoothScrollTo(0,0)
Maybe It's not so elegant way to do it, But this always works for me. Add a new method to the RecyclerView and use it insted of scrollToPosition:
public void myScrollTo(int pos){
stopScroll();
((LinearLayoutManager)getLayoutManager()).scrollToPositionWithOffset(pos,0);
}
The answer is to use the Post Method, it will guarantee correct execution for any action
This is the ultimate solution using kotlin in this date ... if you navigate to another fragment and go back and your recyclerview resets to the first position just add this line in onCreateView or wherever you need can call the adapter...
pagingAdapter.stateRestorationPolicy=RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
BTW pagingAdapter is my adapter with diffUtil.
I had a similar issue, (but not the same), I try to explain it, maybe be could help someone else:
By the time I call to 'scrollToPosition' dataset is already set but some content like images loaded async (using Glide library) and probably when RecyclerView tries to compute the height amount to scroll down, image should return 0 as no loaded yet. So that gives an inaccurate scroll down I could solve it that way:
fun LinearLayoutManager.accurateScrollToPosition(position: Int) {
this.scrollToPosition(position)
this.postOnAnimation {
val realPosition = this.findFirstVisibleItemPosition()
if (position != realPosition) {
this.accurateScrollToPosition(position)
} else {
this.scrollToPosition(position) // this looks redunadant or inecessary but must be call to ensure accurate scroll
}
}
}
PD: In my case was not possible to know the size of the image to be loaded, if you know or you can resize the image you can add a placeholder on glide with de image size or override de size so recyclerView can compute the size correctly and don't need the above walkaraound.
I'm programming with Processing, what i need is the equivalent of pmouseX/Y but for touch, but I can't use pmouse because I use multi-touch and I need previous coordinates of each touched point. I don't know if I've made myself clear, to do an example I need to know initial and final coordinates of a swipe
I currently use to get initial coordinates:
public boolean surfaceTouchEvent(MotionEvent me) {
float x0=me.getX(0);
float y0=me.getY(0);
....
....
return super.surfaceTouchEvent(me);
}
I'm not sure if I get your right here, since this seems to be very basic programming, but I'll try.
Just use an ArrayList and add each position there. For different touches, you might want to use a HashMap, like this:
HashMap<MotionEvent, ArrayList<Point2D.Float>> touches = new HashMap<MotionEvent, ArrayList<Point2D.Float>>();
public boolean surfaceTouchEvent(MotionEvent me)
{
float x0=me.getX(0);
float y0=me.getY(0);
if (!touches.containsKey(me))
{
touches.add(me, new ArrayList<Point2D.Float>());
}
else
{
// get previous position
Point2D.Float prevpos = touches.get(me).get(touches.get(me).size() - 1);
}
touches.get(me).add(new Point2D.Float(x0, y0));
....
}
Didn't test this, but that's basically how it would work.
Would it be possible to hide a view off the top edge of the screen, and only have it appear if the user scrolls upwards?
My first attempt used a scrollview, but it seems that scrollTo() doesn't work unless I used postDelayed (it doesn't even work with Post()). I tried adding it to the scrollview's view tree observer onPreDraw() event and it still doesn't work unless I delay it, so there is an ugly glitch when the activity is first launched.
The second issue is that if the onscreen keyboard is minimized, the view no longer needs to scroll so hiding things by using a scroll offset no longer works. I thought about manipulating the height in code, but this seems pretty hackish.
Is there a better way to do this than by using a scrollview? Alternatively, Does anyone have any tips on the best place to place the scrollTo (the end of onCreate does not work nor the other places I have tried) so I don't need to use postDelayed? That would at least eliminate one glitch.
Thanks!
This is the code I'm using right now, which is the least glitchy but I don't understand why it doesn't work until the third time onPreDraw() is called.
mScrollView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
{
#Override
public boolean onPreDraw()
{
final int fieldYStart = mFieldIWantAtTheTop.getTop();
if (mFieldIWantAtTheTopYStart != fieldYStart
|| mScrollView.getScrollY() < 10)
{
mFieldIWantAtTheTopYStart = fieldYStart;
mScrollView.post(new Runnable()
{
#Override
public void run()
{
Log.v("Testing", "scrolling!");
mScrollView.scrollTo(0, mFieldIWantAtTheTopYStart);
Log.v("Testing", "scroll is now=" + mScrollView.getScrollY());
}
});
}
return true;
}
});
I also tried using a custom scrollview as mentioned below, but this does not solve the issue of the graphical glitch:
#Override
public void onMeasure(int measureWidthSpec, int measureHeightSpec) {
super.onMeasure(measureWidthSpec, measureHeightSpec);
Log.v("Testing", "Scrolling");
post(
new Runnable()
{
public void run()
{
scrollTo(0, 100);
Log.v("Testing", "ScrollY = " + getScrollY());
}
});
}
This code works as does the onPreDraw() code above but there is still a glitch when the activity is launched because the activity is first drawn with the scroll at 0.
I haven't tried this, but you may want to create a custom ScrollView and override onMeasure:
ScrollView scroll = new ScrollView(this) {
#Override
public void onMeasure(int measureWidthSpec, int measureHeightSpec) {
super.onMeasure(measureWidthSpec, measureHeightSpec);
scrollTo(...);
}
};
It seems like this would be the earliest point that scrollTo would be valid.
Edit - I found this answer, which apparently worked for the asker. Is this the method you tried?
I attempted to use a SeekBar for the first time, and I was disappointed with the results. I have an app that needs to use a slider to cover 80 spots. I want the user to be able to move in .5 increments (i.e. 10.5, 11.0). My max range is 40, which is why I doubled it to 80 to account for the decimals.
Everything worked, but when I put it on my Droid, the performance was terrible in my opinion. When trying to stop on 21.5, the slider usually stopped at 22.0 or 22.5, and I had to take extra time to try and inch it over. Other times I would try to select the slider and it was unresponsive. I'm not as concerned about the unresponsiveness, but I can not put the SeekBar in my app if it requires the user to have exact precision to hit a certain spot.
Is there anything that I can do to adjust the SeekBar so that it is easy to hit the number you are trying to land on without difficulty? I'm not sure if the problem is occurring because my SeekBar contains a large amount of possible values, and with screen size limited this forces the numbers to be smashed closer together? Or if this is just the way it behaves in general.
If there isn't a good way to adjust the sensitivity, is there a good alternative that I can use that provides similar functionality?
I have the exact same problem and came up with my own way of reducing the sensitivity. The problem for me happens when I am leaving the slider and it unintentionally gets repositioned, because of thickness of fingers etc. So I wrote a timer to see if the position was selected for a while and snap to this position while leaving the seekbar. This method is a bit clumsy but this is the best I can think of, given I do not want +/- buttons as suggested above. It works well for my needs. Here is the code for anyone interested:
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
int lastProgress = 0;
Timer endTimer = null;
boolean timerExpired = false;
class SeekBarTimerTask extends TimerTask {
public void run() {
timerExpired = true;
}
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if(timerExpired == false) {
// clean up the previous timer and start a new one
setTimer();
}
else {
// if the timer has expired, do not honor small variations of 2%
if(Math.abs(progress - lastProgress) <= seekBar.getMax() * 0.02) {
seekBar.setProgress(lastProgress);
return;
}
else {
// The variation is not small. This means that the seekbar position change
// was intentional. Honor it and schedule the timer again.
setTimer();
}
}
lastProgress = progress;
}
public void onStartTrackingTouch (SeekBar seekBar) {}
public void onStopTrackingTouch (SeekBar seekBar) {
timerExpired = false;
endTimer.cancel();
endTimer.purge();
}
private void setTimer() {
if(endTimer != null) {
endTimer.cancel();
endTimer.purge();
}
timerExpired = false;
endTimer = new Timer();
endTimer.schedule(new SeekBarTimerTask(), 300);
}
});
I'm not sure if the problem is
occurring because my SeekBar contains
a large amount of possible values, and
with screen size limited this forces
the numbers to be smashed closer
together?
Pretty much this. One thing you could do is add a small plus and minus button beside the seekbar, which increment and decrement the value by 0.5. This way you could still use the seekbar for general cases, and refine your selection with the buttons if needed. Maybe not the most elegant solution, but it would certainly work.