viewPager.setCurrentItem() is not working - android

setCurrentItem() function isnt working for following android(java) code how to make it work ?
Intent i = getIntent();
int position = i.getIntExtra("img", 0);
viewPager viewPager = (ViewPager) findViewById(R.id.view_pager);
ImageAdapter adapter = new ImageAdapter(this);
viewPager.setAdapter(adapter);
viewPager.setCurrentItem(position);

Frank is right. To provide the the scroll device independent, you should use onGlobalLayoutListener. Thereby, you can guarantee that the view is rendered when you call viewPager.setCurrentItem(position). Otherwise setCurrentItem won't have any effect since the view is not rendered yet.
viewPager.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
{
#Override
public void onGlobalLayout()
{
viewPager.setCurrentItem(position, false);
}
});
I know the question is already old but maybe someone else will find this usefull!

private ViewPager viewPager;
#Override
public void onResume() {
Intent i = getIntent();
int position = i.getIntExtra("img", 0);
viewPager.postDelayed(new Runnable() {
#Override
public void run() {
viewPager.setCurrentItem(position);
}
}, 100);
}

just adding a shorter answer with androidx using view extension in kotlin
view_pager.doOnLayout {
view_pager.setCurrentItem(position, false)
}
note that the previos answer using global layout is not removing the OnGlobalLayoutListener afterwords which is not advisable.
androidx ext doOnLayout does that.
Please, make sure you have a gradle dependency:
implementation 'androidx.core:core-ktx:1.3.2'

None of these answers worked for me - it took hours for me to find out that I'd forgotten to update getCount().
My getCount() method in my implementation of the FragmentPagerAdapter was displaying the incorrect count, so it could not change page since it was not part of its count.

This worked with me
if (mViewPager.getAdapter() != null)
mViewPager.setAdapter(null);
mViewPager.setAdapter(mPagerAdapter);
mViewPager.setCurrentItem(desiredPos);

Ok, for me, I have been troubleshooting this issue like for 3 hours stright and I got to a point that I made it work this way
Before, I had this inside my ViewPagerStateAdapter
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment {
return when (position) {
1 -> myFragmentInstance1()
else -> myFragmentInstance2()
}
}
Then for changing the pages I was using the following
private fun navigateTo1() {
binding.viewPager.setCurrentItem(1, false)
}
private fun navigateTo2() {
binding.viewPager.setCurrentItem(2, false)
}
First of all after initializing my viewpager adapter like this
private fun setupViewPager() {
binding.viewPager.apply {
adapter = MyViewPagerStateAdapter(this#MainFragment)
}
}
the initial position inside createFragment was 0 , so the else block was always called starting my viewpager with the inscance of myFragmentInstance2()
This is not all, after clicking and trying to setCurrentItem to another position the viewpager was changing only once and then it did not work anymore to switch pages
After long times of debug and research I tried to change the positions of the fragments like this
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> myFragmentInstance1()
else -> myFragmentInstance2()
}
}
which in first place worked for loading 0 as the default destination, and then switching with currentItem worked as expected
private fun navigateTo1() {
binding.viewPager.setCurrentItem(0, false)
}
private fun navigateTo2() {
binding.viewPager.setCurrentItem(1, false)
}
I also tried with
private fun navigateTo1() {
binding.viewPager.currentItem = 0
}
private fun navigateTo2() {
binding.viewPager.currentItem = 1
}
which puts a smoothTransition to true as default and is working fine.
So my conclusion is that the createFragment initializes our different fragments in a position-based way, starting from 0 and if we start creating our fragments at position 1 it will mess the order and then we will end up having just 1 fragment in the stack to switch.
I did lots of research on the issue but did not found anything related to this issue because if we use TabLayoutMediator this issue is not happening, it only happens when we try to switch the viewPager programatically with currentItem

Related

android kotlin what is the current way of make sure viewPager2 pageItem resume is fired only when onscreen?

I want init tasks (starting a countDownTimer) only when the viewPager2 pages become visible.
and stop timer when they get off-screen.
however, I got stuck on the fact that resume() is fired when off-screen for pageItem.
setUserVisibleHint seems deprecated, it seems sexMaxLifeCycle is current way, however, I am not sure how this can be called,
not sure whether call it within... and onHiddenChanged() does not work either.
class ToneFragmentStateAdapter(activity:FragmentActivity):FragmentStateAdapter(activity){
fun setItems(newItems: List<Tone4>) {
_items = newItems
notifyDataSetChanged()
}
private var _items = listOf<Tone4>()
override fun getItemCount(): Int = _items.size
override fun createFragment(position: Int): Fragment
= TonePageFragment.newInstance(position, _items[position])}
ViePager2 creates and attaches fragments off-screen for performance reasons. What you could do is implement a ViewPager2.OnPageChangeCallback and then have a method on your fragment that tells it is the currently active fragment.
For example
fun onPageSelected(position int) {
adapter.items.forEachIndexed( index, fragment -> {
fragment.setActive(position == index)
}
}

Android Navigation Component has lag when navigating through NavigationDrawer

I am updating my app to Navigation Architecture Components and I see that it has a lag replacing fragments which is visible in the NavigationDrawer that does not close smoothly.
Until now, I was following this approach:
https://vikrammnit.wordpress.com/2016/03/28/facing-navigation-drawer-item-onclick-lag/
So I navigate in onDrawerClosed instead than in onNavigationItemSelected to avoid the glitch.
This has been a very common issue, but it is back again. Using the Navigation Component, it is laggy again and I don't see a way to have it implemented in onDrawerClosed.
These are some older answers prior to Navigation Component
Navigation Drawer lag on Android
DrawerLayout's item click - When is the right time to replace fragment?
Thank you very much.
I'm tackling this issue as I write this answer. After some testing, I concluded that code I'm executing in fragment right after its created (like initializing RecyclerView adapter and populating it with data, or configuring UI) is causing the drawer to lag as its all happening simultaneously.
Now the best idea I got is similar to some older solutions that rely on onDrawerClosed. We delay the execution of our code in fragment until the drawer has closed. The layout of the fragment will become visible before the drawer is closed, so it will still look fast and responsive.
Note that I'm also using navigation component.
First, we are going to create an interface and implement it fragments.
interface StartFragmentListener {
fun configureFragment()
}
In activity setup DrawerListener like:
private fun configureDrawerStateListener(){
psMainNavDrawerLayout.addDrawerListener(object: DrawerLayout.DrawerListener{
override fun onDrawerStateChanged(newState: Int) {}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
override fun onDrawerOpened(drawerView: View) {}
override fun onDrawerClosed(drawerView: View) {
notifyDrawerClosed()
}
})
}
To notify a fragment that the drawer has been closed and it can do operations that cause lag:
private fun notifyDrawerClosed(){
val currentFragment =
supportFragmentManager.findFragmentById(R.id.psMainNavHostFragment)
?.childFragmentManager?.primaryNavigationFragment
if(currentFragment is StartFragmentListenr && currentFragment != null)
currentFragment.configureFragment()
}
In case you are not navigating to the fragment from the drawer (for example pressing back button) you also need to notify fragment to do its things. We will implement FragmentLifecycleCallbacksListener:
private fun setupFragmentLifecycleCallbacksListener(){
supportFragmentManager.findFragmentById(R.id.psMainNavHostFragment)
?.childFragmentManager?.registerFragmentLifecycleCallbacks(object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentActivityCreated(fm: FragmentManager, f: Fragment, savedInstanceState: Bundle?) {
super.onFragmentActivityCreated(fm, f, savedInstanceState)
if (!psMainNavDrawerLayout.isDrawerOpen(GravityCompat.START)) {
if (f is StartFragmentListener)
f.configureFragment()
}
}
}, true)
}
In fragment:
class MyFragment: Fragment(), MyActivity.StartFragmentListener {
private var shouldConfigureUI = true
...
override fun onDetach() {
super.onDetach()
shouldConfigureUI = true
}
override fun configureFragment() {
if(shouldConfigureUI){
shouldConfigureUI = false
//do your things here, like configuring UI, getting data from VM etc...
configureUI()
}
}
}
A similar solution could be implemented with a shared view model.
Avoid the lag caused while changing Fragment / Activity onNavigationItemSelected- Android
Navigation Drawer is the most common option used in the applications, when we have more than five options then we go towards the navigation menu.
I have seen in many applications that when we change the option from the navigation menu, we observe that it lags, some people on StackOverflow recommended that use Handler like the below code:
private void openDrawerActivity(final Class className) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
ProjectUtils.genericIntent(NewDrawer.this, className, null, false);
}
}, 200);
}
But in the above code, it’s still not smooth & I thought why we add handler may be there is another solution after so much R&D, what I figure it out that we need to change the fragment/activity when the drawer is going to be close. let’s see with the implementation.
For more details with solution kindly go through https://android.jlelse.eu/avoid-the-lag-caused-while-changing-fragment-activity-onnavigationitemselected-android-28bcb2528ad8. It really help and useful.
Hope you find better solutions in it!

Android Espresso testing SwipeRefreshLayout OnRefresh not been triggered on swipeDown

I'm trying to write simple test for pull to refresh as a part of integration testing. I'm using the newest androidX testing components and Robolectric. I'm testing isolated fragment in which one I'm injecting mocked presenter.
XML layout part
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerTasks"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
Fragment part
binding.refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
presenter.onRefresh();
}
});
Test:
onView(withId(R.id.refreshLayout)).perform(swipeDown());
verify(presenter).onRefresh();
but test doesn't pass, message:
Wanted but not invoked: presenter.onRefresh();
The app works perfectly fine and pull to refresh calls presenter.onRefresh(). I did also debugging of the test and setOnRefreshListener been called and it's not a null. If I do testing with custom matcher to check the status of SwipeRefreshLayout test passes.
onView(withId(R.id.refreshLayout)).check(matches(isRefreshing()));
I did some minor investigation over last weekend since I was facing the same issue and it was bothering me. I also did some comparing with what happens on a device to spot the differences.
Internally androidx.swiperefreshlayout.widget.SwipeRefreshLayout has an mRefreshListener that will run when onAnimationEnd is called. The AnimationEnd will trigger then OnRefreshListener.onRefresh method.
That animation listener (mRefreshListener) is passed to the mCircleView (CircleImageView) and the circle animation start is called.
On a device when the view draw method is called it will call the applyLegacyAnimation method that will, in turn, call the AnimationStart method. At the AnimationEnd, the onRefresh method will be called.
On Robolectric the draw method of the View is never called since the items are not actually drawn. This means that the animation will never run and thus neither will the onRefresh method.
My conclusion is that with the current version of Robolectric is not possible to verify that the onRefresh called due to implementation limitations. It seems though that it is planned to have a realistic rendering in the future.
I'm finally able to solve this using a hacky way :
fun swipeToRefresh(): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return object : BaseMatcher<View>() {
override fun matches(item: Any): Boolean {
return isA(SwipeRefreshLayout::class.java).matches(item)
}
override fun describeMismatch(item: Any, mismatchDescription: Description) {
mismatchDescription.appendText(
"Expected SwipeRefreshLayout or its Descendant, but got other View"
)
}
override fun describeTo(description: Description) {
description.appendText(
"Action SwipeToRefresh to view SwipeRefreshLayout or its descendant"
)
}
}
}
override fun getDescription(): String {
return "Perform swipeToRefresh on the SwipeRefreshLayout"
}
override fun perform(uiController: UiController, view: View) {
val swipeRefreshLayout = view as SwipeRefreshLayout
swipeRefreshLayout.run {
isRefreshing = true
// set mNotify to true
val notify = SwipeRefreshLayout::class.memberProperties.find {
it.name == "mNotify"
}
notify?.isAccessible = true
if (notify is KMutableProperty<*>) {
notify.setter.call(this, true)
}
// mockk mRefreshListener onAnimationEnd
val refreshListener = SwipeRefreshLayout::class.memberProperties.find {
it.name == "mRefreshListener"
}
refreshListener?.isAccessible = true
val animatorListener = refreshListener?.get(this) as Animation.AnimationListener
animatorListener.onAnimationEnd(mockk())
}
}
}
}

HOW do you invalidate a fragment view?

Alright, so I have a fragment that displays user account information.
The way this works is it issues a request that may or may not touch the server, depending on what information is already cached.
When the information is available, there's a callback in which the fragment will update its textfields.
At that point, however, the UI needs be refreshed, so I'd like to issue some kind of invalidate...
Except that does perfectly nothing.
First, I noticed that this.view returns null. So rather than rely on that, I store the view I create in onCreateView explicitly.
Then after I updated the textfields, I call fragmentView.postInvalidate() ... which does nothing.
I also tried doAsync { uiThread { fragmentView.invalidate() } } ... which also does nothing.
Then I found this answer and tried
val fragment = this
doAsync { uiThread {
activity!!.supportFragmentManager.beginTransaction().detach(fragment).commit()
activity!!.supportFragmentManager.beginTransaction().attach(fragment).commit()
}}
... which is even WORSE than not doing anything, because the other two ways will at least update the view if you switch to a different fragment and back, whereas this will perpetually refuse to display any useful information.
So maybe I can "cheat" by delaying the call to super::setUserVisibleHint until after I updated the values ... no, I can't.
What I can do, though, is I can force a redraw of the ui by toasting something. So that's what I am currently doing. But still, that can't be the solution.
How do I get a proper ui refresh?
Oh, and this is an android.support.v4.app.Fragment and I'm using an android.support.v4.app.FragmentStatePagerAdapter to switch between fragments, if that matters.
Full fragment code:
class AccountFragment : Fragment() {
lateinit var fragmentView: View
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
val view = inflater.inflate(R.layout.fragment_account, container, false)
/*for some reason [this.view] remains null when we later try to use it, so we're explicitly storing a reference*/
fragmentView = view
return view
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
if(isVisibleToUser)setAccountInformation()
super.setUserVisibleHint(isVisibleToUser)
}
private fun setAccountInformation(){
val um = DataConfig.getUserManager()
val au = um.getActiveUser()
um.getUserInfo(au, Callback(
onResponse = {when(it){
is SuccessfulAccountGetResponse -> {
val info = it.result
usernameText.text = info.name
uuidText.text = info.uuid
adminText.text = if(info.isAdmin) "YES" else "NO"
createdText.text = info.created.toString()
lastLoginText.text = info.lastLogin?.toString() ?: "-"
//now actually force the new information to show up
refreshUI()
}
else -> doAsync { uiThread{ activity?.longToast("could not get information") } }
}},
onError = {doAsync { uiThread { activity?.longToast("error: $it") } }}
))
}
/** forces a refresh, making changes visible*/
private fun refreshUI(){
/*hack: force a redraw by toasting */
doAsync { uiThread { activity?.toast("updating values") } }
//THIS does absolutely nothing: //TODO: figure out why
// fragmentView.postInvalidate()
}
}

Loading picture to correct position in recycler view

I make a network call in my recycler adapter to retrieve the url for a picture. After the url is received, I use universal image loader to load the picture into an image view. The problem is when I don't scroll the pictures are loaded into the right place but as soon as I scroll the pictures are inflated in the wrong place.
Here's my adapter:
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder) {
val article = feeds[position]
holder.articleTitle.setFont("SourceSansPro-SemiBold.ttf")
holder.articleDescription.setFont("OpenSans-Regular.ttf")
holder.articleTime.setFont("OpenSans-Light.ttf")
mAnimator?.onBindViewHolder(holder.itemView, position)
holder.apply {
article.apply {
articleTitle.text = title
articleDescription.text = Html.fromHtml(description)
articleTime.text = TimeUtils.convertLongToTime(pubDate)
if (image.isBlank()){
//load picture url when it's empty
mContext?.doAsync {
ImageExtractor.extractImageUrl(link, object : OnImageExtractorListener {
override fun onSuccess(url: String) {
v("imaaaage success $title $url")
mContext?.runOnUiThread {
article.image = url
//use uil to load the image didn't work so I tried just updating the model
//articleImage.displayImage(url)
feeds[position] = article
notifyItemChanged(position)
}
val dbo = context.getDatabase()
dbo.updateArticleImage(dbo,url,article.id)
}
override fun onError() {
}
})
}
}else{
articleImage.displayImage(image)
isRead?.let {
if (isRead!! && !isSaved){
grayScale(holder)
}
}
}
container.setOnClickListener {
itemClick(this)
if (!isSaved){
article.isRead = true
feeds[position] = article
notifyItemChanged(position)
}
}
}
}
}else if (holder is LoadingViewHolder){
holder.progressBar.isIndeterminate = true
}
}
I need a way to load the images in their right places if the user is scrolling or not.
Consider using a library for async image loading, e.g. Picasso. There everything is handled for you, like caching, placeholder ...
In your adapter:
Picasso.with(context).load("url")
.placeholder(R.drawable.user_placeholder).into(imageView);
Gradle:
compile 'com.squareup.picasso:picasso:2.5.2'
That's all!
you nedd to put setHasStableIds(true); in your Adapter's constructor and put :
#Override
public int getItemViewType(int position) {
return position;
}
#Override
public long getItemId(int position) {
return position;
}
So it will keep all images at exact positions even after scroll.
The RecyclerView will reuse ViewHolders to reduce inflation and memory usage.
The correct way is in the onBindViewHolder is to clear the old state of the View (seting image as null) and seting the new ones.
Make sure to clear it all times it come onBind (before testing)
articleImage.displayImage(null)
if (image.isBlank()){
Since downloading is a async task it will not be at the way when binding, that's why you just clear the old one, for texts it is immediately available to be set.

Categories

Resources