问题
I am facing this rather strange problem with viewPager2. I did some reading and found out that viewPager2 has a default offset limit of 0 which is perfect for my application. I'm using it with a tab layout and I have 3 fragments (Home, Profile, Notification). When the activity loads and the first fragment(Home) loads, I can see in my logcat that the next fragment(Profile) is not loaded, as expected. But when I click on the profile tab something strange happens, the next tab(Notification) is preloaded. The methods onAttach,onCreate,onCreateView,onActivityCreated,onStart for the Notification tab is called. Why is this doing so and how to I fix this ? Logcat Screenshot I have attached a screen shot of my logcat here. Thankyou in advance.
回答1:
I Assume you mean OFFSCREEN_PAGE_LIMIT_DEFAULT
not offsetlimit
as you are talking about a preloading of fragments problem.
And the default value is -1
not zero https://developer.android.com/reference/androidx/viewpager2/widget/ViewPager2.html#OFFSCREEN_PAGE_LIMIT_DEFAULT
and the default means
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.
As this is a performance optimisation of the recyclerview, I would say it's not a guarantee that it won't preload your fragments, it's just left to the caching mechanism of the recyclerview to decide.
There are a number of factors that can affect the recyclerview's caching mechanism.
If preloading of your fragment is a problem because you have dynamic data in it that you only want to be loaded when the page is shown then it would be better to move your fragment to use a "lazy loading" method i.e. only load the data when it is shown.
I had a similar problem with the original viewpager and solved it with "lazy loading". If the timing of loading of your dynamic data is the problem then update the question and then I can outline a possible solution.
Update:3
It seems that Viewpager2 actually works correctly with the Fragments lifecycle unlike the original Viewpager thus you can call updateView()
as shown in update2 example from the Fragments onResume
method without having to use the pageSelected
callback via the Adapter to trigger the update of the View.
Update:
I believe the actual cause from looking at the viewpager2 code is that selecting the Tab does a fake drag with smooth scrolling and smooth scrolling adds the selected item +1 to the cache, if you swipe from Tab0 to Tab1 yourself it does not create Tab2
ViewPager2 is a bit different and "lazy loading" method I used for the original ViewPager does not totally fit but there is a slightly different way to do the same.
The main idea with the original ViewPager was to update the view ONLY when a page was selected using a onPageChangeListener but ViewPager2 uses a callback instead
So add the following after you have created the ViewPager2 (in the Activity onCreate usually)
viewPager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
// Tell the recyclerview that position 2 has changed when selected
// Thus it recreates it updating the dynamic data
if (position == 2) {
// the adapter for the ViewPager needs to a member of the Activity class so accessible here
adapter.notifyItemChanged(position);
}
}
});
This is simpler but has a minor drawback that the dynamic data is loaded when it is preloaded and then again when it is actually displayed.
Update2: A more efficient addition to first method more similar to my original approach
This is a full working example as it is easier to explain.
The main idea is in your fragment with the dynamic data that you ONLY want to load when it is displayed is to create an empty "placeholder" view item and you don't fill it with data in the Fragments onViewCreated
, in this example it is a second textview
with no text but could be a recyclerview with zero objects or any other type of view.
In your Fragment you then create a method to update the "placeholder" with the data (in this case the method is called updateView()
which sets the textview text to the current date and time)
Then in your Fragment Adapter you store a reference to each fragment it creates in a ArrayList
(this allows you get the fragment back) and you then create an updateFragment()
method in the adapter that uses the position to get the Fragment to be able to call the updateView()
on it.
Finally again you use onPageSelected
to call the updateFragment
with the position you want to dynamically update.
So textview1
shows the data and time the fragement was created and textview2
is only shown on the third Tab and has a date and time on when page was selected. Note that texview1
on "Tab 2" and "Tab 3" is the same time when you click on the "Tab 2" headers to change tabs (this is the problem in the question)
MainActivity.java
package com.test.viewpager2;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager2.widget.ViewPager2;
import android.os.Bundle;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
public class MainActivity extends AppCompatActivity {
TabLayout tabLayout;
ViewPager2 viewPager2;
ViewPager2Adapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewPager2 = findViewById(R.id.viewpager2);
tabLayout = findViewById(R.id.tabLayout);
viewPager2.setAdapter(createCardAdapter());
new TabLayoutMediator(tabLayout, viewPager2,
new TabLayoutMediator.TabConfigurationStrategy() {
@Override public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
tab.setText("Tab " + (position + 1));
}
}).attach();
viewPager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
// Tell the recyclerview that position 2 has changed when selected
// Thus it recreates it updating the dynamic data
if (position == 2) {
adapter.updateFragment(position);
}
}
});
}
private ViewPager2Adapter createCardAdapter() {
adapter = new ViewPager2Adapter(this);
return adapter;
}
}
ViewPager2Adapter.java
package com.test.viewpager2;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import java.util.ArrayList;
public class ViewPager2Adapter extends FragmentStateAdapter {
private static final int numOfTabs = 3;
private ArrayList<Fragment> fragments = new ArrayList<>();
public ViewPager2Adapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
}
@NonNull @Override public Fragment createFragment(int position){
Fragment fragment = TextFragment.newInstance(position);
fragments.add(fragment);
return fragment;
}
@Override
public int getItemCount(){
return numOfTabs;
}
public void updateFragment(int position){
Fragment fragment = fragments.get(position);
// Check fragment type to make sure it is one we know has an updateView Method
if (fragment instanceof TextFragment){
TextFragment textFragment = (TextFragment) fragment;
textFragment.updateView();
}
}
}
TextFragment.java
package com.test.viewpager2;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
public class TextFragment extends Fragment {
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private static final String ARG_PARAM1 = "param1";
private int mParam1;
private View view;
public TextFragment() {
// Required empty public constructor
}
public static TextFragment newInstance(int param1) {
TextFragment fragment = new TextFragment();
Bundle args = new Bundle();
args.putInt(ARG_PARAM1, param1);
fragment.setArguments(args);
Log.d("Frag", "newInstance");
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getInt(ARG_PARAM1);
}
Log.d("Frag", "onCreate");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
Log.d("Frag", "onCreateView");
view = inflater.inflate(R.layout.fragment_text, container, false);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
Bundle args = getArguments();
TextView textView1 = view.findViewById(R.id.textview1);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss.sss", Locale.US);
String dt = df.format(Calendar.getInstance().getTime());
textView1.setText(dt);
}
public void updateView(){
Log.d("Frag", "updateView");
TextView textView2 = view.findViewById(R.id.textview2);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss.sss", Locale.US);
String dt = df.format(Calendar.getInstance().getTime());
textView2.setText(dt);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
Log.d("Frag", "onAttach");
}
@Override
public void onDetach() {
super.onDetach();
Log.d("Frag", "onDetach");
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager2"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TextFragment">
<TextView
android:id="@+id/textview1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textview2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview1" />/>
</androidx.constraintlayout.widget.ConstraintLayout>
来源:https://stackoverflow.com/questions/60252055/viewpager2-preloaing-next-fragment-even-when-offsetlimit-is-the-default0