巢狀滾動多TAB可懸浮頭效果實現
前言
在前面的文章中我們已經實現過巢狀滾動可以懸浮頭效果,當時有兩種實現:
1. Listview多tab上滑懸浮 一種是一個ListView裡面切換資料來源,同時監控頁面滾動,佈局頁面中設定兩層,一層放置懸浮頭,滾動到一定位置時,顯示出懸浮頭
2. 多TAB可懸浮頭控制元件還有一種是上面懸浮頭部內容,底層多tab採用viewpager來實現,在viewpager裡面的listview中插入相同大小的頭部,監控listview的滾動來同步滾動頭部內容。
上面兩種方式都能實現上面的效果,但都有比較大的缺點,第一種當有個tab時,資料切換複雜,需要處理的狀態太多,第二種也需要處理很多種回撥情況,計算各種偏移量。
這裡我們採用系統原生控制元件,來採用一種新的實現方式。
效果
這裡我們來看看實現後的效果。由於gif每次生成的都很大,傳不上來,只好截幾張圖了(誰有好辦法可以分享一下,gif尺寸小了太糊,大了傳不上來)
上圖第一張是初始進入效果,第二張是部分滑動後效果
![]()
![]()
第三圖是上劃到一定程度Tab懸浮效果,第四圖是下拉重新整理效果
實現
上面我們已經看了效果圖,因此我們來實現一下,這裡主要採用了系統的SwipeRefreshLayout來實現重新整理效果,CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout+NestedScrollView來實現頭部懸浮,與底部內容巢狀滾動效果,這裡我們不需要引入外部控制元件,也不需要監控太多狀態就能實現Tab懸浮頭效果
佈局
首先我們來看看佈局內容
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context="com.demo.example.activity.NestScrollingActivity">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/global_fg_color"
app:titleTextColor="@color/white"/>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/refresh_layout"
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">
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:contentScrim="@color/global_fg_color"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<include layout="@layout/include_scroll_head"></include>
</android.support.design.widget.CollapsingToolbarLayout>
<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/global_fg_color"
app:tabIndicatorColor="#FFFFFFFF"
app:tabSelectedTextColor="#FF888888"
app:tabTextColor="#FFFFFFFF"></android.support.design.widget.TabLayout>
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_nest_scrolling"/>
</android.support.design.widget.CoordinatorLayout>
</android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>
這裡我們沒有把Toolbar放到CollapsingToolbarLayout,放到CollapsingToolbarLayout裡面是實現Toolbar效果的常用實現,這裡我們主要將Toolbar固定位置,其他內容全部滾動摺疊(這裡採用不同的實現,可以實現不同的效果,可以實現沉浸式狀態列,標題隨著頁面滾動顯示不同的效果。我已經實現了沉浸式效果,如有需要留言)
我們將頭部內容都放置到CollapsingToolbarLayout,我們採用include方式引入,內容如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
android:id="@+id/feed_detail_content"
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="wrap_content"
android:background="@color/global_bg_color"
android:paddingBottom="@dimen/dp_size_18"
android:paddingLeft="@dimen/dp_size_10"
android:paddingRight="@dimen/dp_size_10"
android:paddingTop="@dimen/dp_size_18">
<ImageView
android:id="@+id/imageView_head"
android:layout_width="@dimen/head_image"
android:layout_height="@dimen/head_image"
android:src="@drawable/avatar_default_head"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/textView_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/dp_size_12"
android:lines="1"
android:text="使用者暱稱"
android:textColor="@color/white"
android:textSize="@dimen/sp_size_16"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView_head"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/textView_role"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:lines="1"
android:text="描述資訊"
android:textColor="@color/white_opacity_50"
android:textSize="@dimen/sp_size_10"
app:layout_constraintBottom_toBottomOf="@+id/imageView_head"
app:layout_constraintEnd_toEndOf="@+id/textView_name"
app:layout_constraintStart_toStartOf="@+id/textView_name"/>
<TextView
android:id="@+id/textView_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="@dimen/dp_size_12"
android:text="我是內容,很長很長的內容"
android:textColor="@color/white_opacity_80"
android:textSize="@dimen/sp_size_14"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView_head"/>
<ImageView
android:id="@+id/image_content"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_marginTop="@dimen/dp_size_10"
android:scaleType="centerCrop"
android:src="@drawable/beautify"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView_content"></ImageView>
<TextView
android:id="@+id/textView_source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lines="1"
android:paddingTop="@dimen/dp_size_10"
android:text="13.30 我是底部文案"
android:textColor="@color/white_opacity_30"
android:textSize="@dimen/sp_size_12"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image_content"/>
</android.support.constraint.ConstraintLayout>
頭部內容我們採用了ConstraintLayout來佈局,你可以採用任何佈局來實現你想要的效果,這裡只是一個樣例。
最上面的佈局中還有一部分content_nest_scrolling,這個是除標題為其他的內容,內容如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
android:id="@+id/nested_scroll_view"
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"
android:background="@color/colorAccent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.demo.example.activity.NestScrollingActivity"
tools:showIn="@layout/activity_nest_scrolling">
<android.support.v4.view.ViewPager
android:id="@+id/data_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/global_bg_color"
android:nestedScrollingEnabled="false"></android.support.v4.view.ViewPager>
</android.support.v4.widget.NestedScrollView>
這裡要注意的是,內容必須放到NestedScrollView才能實現巢狀滾動,上面的內容滾動後,頁面佈局發生了變化,因此需要NestedScrollView來處理變化的效果,由於我們是多個Tab,因此用Viewpage來實現,Tab的效果用TabLayout來實現,他可以與ViewPager實現聯動,你也可以採取其他的實現。
介面實現
佈局有了,我們再來實現Activity介面:
package com.demo.example.activity;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.widget.NestedScrollView;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import com.demo.example.R;
import com.demo.example.fragment.TabFragment;
import com.demo.example.util.LogUtil;
public class NestScrollingActivity extends AppCompatActivity {
private ViewPager viewPager;
private TabLayout tabLayout;
private NestedScrollView scrollView;
private SwipeRefreshLayout refreshLayout;
private AppBarLayout appBarLayout;
private Handler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nest_scrolling);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
findViews();
init();
}
private void findViews() {
viewPager = findViewById(R.id.data_pager);
tabLayout = findViewById(R.id.tab_layout);
scrollView = findViewById(R.id.nested_scroll_view);
refreshLayout = findViewById(R.id.refresh_layout);
appBarLayout = findViewById(R.id.app_bar);
}
private void init() {
handler = new Handler(getMainLooper());
appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
if (!refreshLayout.isRefreshing()) {
refreshLayout.setEnabled(verticalOffset == 0);
}
LogUtil.e("verticalOffset=" + verticalOffset);
}
});
viewPager.setAdapter(new TabAdapter(getSupportFragmentManager()));
viewPager.setOffscreenPageLimit(3);
tabLayout.setupWithViewPager(viewPager);
refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
handler.postDelayed(new Runnable() {
@Override
public void run() {
refreshLayout.setRefreshing(false);
}
}, 500);
}
});
}
private class TabAdapter extends FragmentPagerAdapter {
public TabAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return new TabFragment();
}
@Override
public int getCount() {
return 3;
}
@Override
public CharSequence getPageTitle(int position) {
return "TAB" + position;
}
}
}
介面中沒有太多需要實現的東西,主要是給設定ViewPager裡面的內容,沒有這裡放置三個Fragment。這裡我們主要看看以下內容:
appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
if (!refreshLayout.isRefreshing()) {
refreshLayout.setEnabled(verticalOffset == 0);
}
LogUtil.e("verticalOffset=" + verticalOffset);
}
});
這裡我們給appBarLayout設定一個OnOffsetChangedListener來監聽appBarLayout滾動的距離,為什麼要設定這個? 這是由於我們最外層嵌套了一層SwipeRefreshLayout, SwipeRefreshLayout實現了下拉重新整理的效果,因此你往上滾的時候是沒什麼問題的,但是向下滾動時,由於內容嵌套了滾動控制元件,會導致下拉重新整理的觸發時機不對,會在頁面中途就出現重新整理效果,因此我們只有到appBarLayout的內容完全展示的時候,才設定SwipeRefreshLayout可重新整理。
還有一部分內容我們來看看:
refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
handler.postDelayed(new Runnable() {
@Override
public void run() {
refreshLayout.setRefreshing(false);
}
}, 500);
}
});
上述的程式碼又是幹什麼的?由於SwipeRefreshLayout實現了重新整理效果,但是是需要手動來停止該效果的,因此上面只是模擬一個重新整理的效果。實際運用中,由於重新整理是在最外層的,而重新整理的內容一般在Fragment中,因此你需要呼叫Fragment進行內容重新整理,當時重新整理完成時你需要回調頁面停止,這裡沒有采用callback方式實現,也可以採用google新的架構元件LiveData來實現,用LiveData可以實現解耦。
Fragment
最後我們來看看TabFrament的實現:
public class TabFragment extends Fragment {
RecyclerView recyclerView;
private List<String> datas;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle
savedInstanceState) {
return inflater.inflate(R.layout.fragment_tab_layout, container, false);
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
findViews();
init();
}
private void findViews() {
recyclerView = getView().findViewById(R.id.list);
}
private void init() {
getDatas();
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setAdapter(new BaseAdapter<String>(datas, new BaseDelegate() {
@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new StringViewHolder(parent, getItemView(parent, viewType));
}
@Override
public int getItemViewType(Object data) {
return 0;
}
@Override
public int getLayoutId(int viewType) {
return R.layout.tab_item_view_holder;
}
}));
}
private class StringViewHolder extends BaseViewHolder<String> {
private TextView textView;
/**
* @param parent current no use, may be future use
* @param view
*/
public StringViewHolder(ViewGroup parent, View view) {
super(parent, view);
}
@Override
public void findViews() {
textView = itemView.findViewById(R.id.content);
}
@Override
public void onBindViewHolder(String data) {
textView.setText(data);
}
}
private void getDatas() {
datas = new ArrayList<>();
for (int i = 0; i < 20; ++i) {
datas.add("content" + i);
}
}
這裡就是一般的實現,一個recyclerView實現列表效果。沒有太多複雜的東西。根據你的需要進行實現。