安卓TabLayout,ViewPager以及fragment的使用
Demo效果
首先先說一下這個demo的最終效果吧:
項目地址:https://github.com/xiaohuiduan/fragment_demo

我們的安卓作業需要我們結合RecyclerView,TabLayout,PaperView以及fragment做一個簡單的Demo,於是便有了這樣的一個demo,其中頁面中的數據來自於玩安卓的開放API,使用的是其中的公眾號接口。
-
tabLayout :在圖中表示的上面的能夠進行滑動的tab。
-
fragment:
fragment 簡單點來說就是一個模塊,類似activity,在里面可以放一些其他的組件(比如說textView等等),並且有着自己的生命周期。但是它必須放在activity中間,具體的一些信息,可以去看一看官方文檔中文,英文(推薦看英文的,感覺中文的就是機翻,怪怪的)
-
viewPaper:
這個是一段來自官網的介紹
ViewPaper就是簡單的頁面切換組件,我們往里面填充View,然后就可以使用左滑和右滑來進行View的切換。和RecycleView(或者ListView)很類似,我們都需要使用Adapter(PagerAdapter)來填充數據。google官方文檔中,推薦我們使用Fragment來填充ViewPager。
因此簡單點說,就是TabLayout和ViewPager相關聯,然后通過滑動ViewPager實現Fragment的切換,然后在Fragment中使用RecycleView來顯示數據。
主要代碼分析
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" android:orientation="vertical">
<com.google.android.material.tabs.TabLayout android:id="@+id/tablayout" android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto" app:tabMode="scrollable" xmlns:android="http://schemas.android.com/apk/res/android" />
<androidx.viewpager.widget.ViewPager android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/article_title_view_pager" />
</LinearLayout>
MainActivity的代碼
public class MainActivity extends AppCompatActivity {
private TabLayout tabLayout;
private ViewPager viewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewPager = findViewById(R.id.article_title_view_pager);
init();
}
private void init() {
// 下面的這個代表目前是橫屏還是豎屏,不需要理會
if (findViewById(R.id.land_content) != null) {
PhoneMsg.isTwoPane = true;
} else {
PhoneMsg.isTwoPane = false;
}
initTab();
}
public void initTab() {
try {
// 下面的代表獲取數據,我們只需要知道PhoneMsg.wxAuthorList保存了文章的信息即可
AsyncTask<ProcessInterface, Integer, Object> asyncTask = new AsyncRequest().execute(new GetWxAuthorList());
PhoneMsg.wxAuthorList = (WxAuthorList) asyncTask.get();
tabLayout = findViewById(R.id.tablayout);
// 創建Tab頁
for (int i = 0; i < PhoneMsg.wxAuthorList.getData().size(); i++) {
tabLayout.addTab(tabLayout.newTab());
}
// ArticleTitlePageAdapter表示的就是ViewPager的適配器(后面會說明),其中我們需要傳過去的參數是,getSupportFragmentManager():為了拿到Fragment的控制器, tabLayout.getTabCount(),獲得view的個數
ArticleTitlePageAdapter adapter = new ArticleTitlePageAdapter(getSupportFragmentManager(), tabLayout.getTabCount());
viewPager.setAdapter(adapter);
// 這一步是為了將tabLayout與viewpaper同步,值得注意的點是:如果將這一步放在tablayOut的setText后面,則會導致tab的名字空白
tabLayout.setupWithViewPager(viewPager);
// 創建Tab頁
for (int i = 0; i < PhoneMsg.wxAuthorList.getData().size(); i++) {
String name = PhoneMsg.wxAuthorList.getData().get(i).getName();
int id = PhoneMsg.wxAuthorList.getData().get(i).getId();
tabLayout.getTabAt(i).setText(name);
// ArticleListAdapter代表的是RecycleView的適配器,我們將這個Adapter保存起來(為什么要這樣做我后面說)
PhoneMsg.articleListAdapters.add(i,new ArticleListAdapter(MainActivity.this,id));
}
// 添加選擇事件
tabLayout.addOnTabSelectedListener(new TabClick(this));
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
class TabClick implements TabLayout.OnTabSelectedListener {
Context context;
public TabClick(Context context) {
this.context = context;
}
@Override
public void onTabSelected(TabLayout.Tab tab) {
// 將viewPaper的位置改變
viewPager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
}
}
ViewPager的適配器
這個適配器的功能很簡單,就是為了返回Fragment去填充ViewPager。可以看到下面的代碼,就是根據不同position來產生不同的Fragment。
public class ArticleTitlePageAdapter extends FragmentStatePagerAdapter {
private int num;
private HashMap<Integer,ArticleTitleFra> map;
public ArticleTitlePageAdapter(@NonNull FragmentManager fm, int num) {
super(fm, num);
this.num = num;
map = new HashMap(num);
}
/** * 返回對應位置的Fragment * @param position 代表目前滑動所處的位置 * @return */
@NonNull
@Override
public Fragment getItem(int position) {
if (map.containsKey(position)){
return map.get(position);
}
return createFragment(position);
}
/** * 創建一個frame * @param position * @return */
private Fragment createFragment(int position) {
// ArticleTitleFra.newInstance(position)會返回一個Fragment
map.put(position, ArticleTitleFra.newInstance(position));
return map.get(position);
}
@Override
public int getCount() {
return num;
}
}
下面就是關於Fragment的適配器,其中在Fragment中是一個RecycleView。
Fragment的適配器
public class ArticleTitleFra extends Fragment {
private View view;
private RecyclerView recyclerView;
private ArticleListAdapter adapter;
public static ArticleTitleFra newInstance(int position) {
Bundle bundle = new Bundle();
bundle.putInt("Position", position);
ArticleTitleFra articleTitleFra = new ArticleTitleFra();
articleTitleFra.setArguments(bundle);
return articleTitleFra;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
int position = getArguments().getInt("Position");
// PhoneMsg.articleListAdapters里面保存了RecycleView的適配器
adapter = PhoneMsg.articleListAdapters.get(position);
// article_title_fra表示的就是Fragment 的xml文件
view = inflater.inflate(R.layout.article_title_fra, container, false);
// 設置recyclerView
initFrame();
// refresh()目的是為了設置recycleView的Adapter
refresh();
return view;
}
private void initFrame() {
recyclerView = view.findViewById(R.id.article_list);
recyclerView.setLayoutManager(new LinearLayoutManager(view.getContext()));
// 設置 ItemAnimator動畫
recyclerView.setItemAnimator(new DefaultItemAnimator());
}
/** * 刷新數據 * * @param */
public void refresh() {
recyclerView.setAdapter(this.adapter);
}
}
RecycleView的適配器我就不展示code了。在這里個里面我們有一個值得注意的點,在Fragment中,我們是使用了一個newInstance來示例化一個對象,並且將position保存在Bundle中,為什么我們這樣做呢?假設我們不這樣做,那么在橫豎屏切換的時候會產生一個問題。就是在豎屏切換到橫屏的時候,recycleView的數據會消失。(值得注意點的是:在MainActivity中onCreate方法會再次執行一遍)。按照道理來說,不應該出現這種情況的,那么為什么會出現這種情況呢?初略的瀏覽了一下源代碼,我覺的可能是這樣的:
FragmentStatePagerAdapter中會將Fragment進行序列化,在加載新的頁面的時候,它會看以前有沒有進行加載,如果加載了,則從序列化的數據中將Fragment拿出來。我們可以用Log進行日志輸出,然后會發現,在橫豎屏轉動的時候,這一個日志並不會進行打印。
那么,為什么我們使用newInstance來實例化對象而不是通過構造方法來示例化對象呢?很可惜,不行,因為在Fragment中,會通過反射調用Fragment的無參構造函數來獲得Fragment的實例。所以如果我們在Fragment中只寫了有參構造器而沒有寫無參構造器,那么程序則會報
java.lang.RuntimeException: Unable to start activity ComponentInfo{cc.weno.android_news/cc.weno.android_news.MainActivity}: androidx.fragment.app.Fragment$InstantiationException: Unable to instantiate fragment cc.weno.android_news.fragment.ArticleTitleFra: could not find Fragment constructor
的錯誤。而在Fragment序列化的過程中,會將Bundle進行序列化,而在反序列化中,又會將boundlei進行反序列化,so,我們就可以在創建實例化的過程中將同時設置Bundle,因此下面的情況也是ok的。
public ArticleTitleFra(){ } public ArticleTitleFra(int position){ Bundle bundle = new Bundle(); bundle.putInt("Position", position); setArguments(bundle); }
這也就解釋了為什么我使用PhoneMsg.articleListAdapters
來保存recycleView的適配器了,因為在Fragment createView的時候,我們根據position的位置來獲得對應的適配器。那么我們將recycleview的適配器添加到Bundle中呢?當然沒有問題,不過我們就需要對recycleview的適配器進行序列化了。

總的來說這個作業還是比較簡單的,只不過在這個序列化的這個地方被坑了好久,查問題感覺自己的頭發都掉了好多。