1. 程式人生 > >巢狀滾動多TAB可懸浮頭效果實現

巢狀滾動多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實現列表效果。沒有太多複雜的東西。根據你的需要進行實現。

結語