在列表滾動的時候顯示或者隱藏Toolbar(二)

在第一部分中,咱們學會了如何實現Google+應用中隱藏Toolbar的效果,今天咱們來實現Play Store中的效果。html

在開始以前,我先講講這一部分對 項目  結構的一點改動。原有的activity被分割成了兩個:PartOneActivity和PartTwoActivity,他們都是被MainActivity所調用。java

譯者注:在閱讀本文的同時,最好先實際操做一下play store應用,即使你大體知道效果是怎樣也建議操做一下,否則下面的計算有點很差理解。其實這些都是很細微的東西,要一眼帶過,估計什麼也看不出來。android

開始

build.gradle文件和第一部分是同樣的,再也不贅述,咱們從建立Activity的佈局開始:git

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android"
     android:layout_width= "match_parent"
     android:layout_height= "match_parent" >
  
     <android.support.v7.widget.RecyclerView
         android:id= "@+id/recyclerView"
         android:layout_width= "match_parent"
         android:layout_height= "match_parent"
         android:paddingTop= "?attr/actionBarSize"
         android:clipToPadding= "false" />
     <android.support.v7.widget.Toolbar
         android:id= "@+id/toolbar"
         android:layout_width= "match_parent"
         android:layout_height= "?attr/actionBarSize"
         android:background= "?attr/colorPrimary" />
  
</FrameLayout>

只有一個RecyclerView和一個Toolbar(後面咱們還會添加Tabs)。注意此次咱們使用的是 上篇文章  中提到的第二種方法(添加padding到RecyclerView)。list item的佈局文件和上次同樣,直接跳過,RecyclerAdapter一樣如此(這裏   是其代碼-一個不帶header的adapter),跳過 ,咱們直接進入PartTwoActivity的代碼:github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class PartTwoActivity extends ActionBarActivity {
  
     private Toolbar mToolbar;
      
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         setTheme(R.style.AppThemeGreen);
         super .onCreate(savedInstanceState);
         setContentView(R.layout.activity_part_two);
          
         initToolbar();
         initRecyclerView();
     }
      
     private void initToolbar() {
         mToolbar = (Toolbar) findViewById(R.id.toolbar);
         setSupportActionBar(mToolbar);
         setTitle(getString(R.string.app_name));
         mToolbar.setTitleTextColor(getResources().getColor(android.R.color.white));
     }
      
     private void initRecyclerView() {
         final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
         recyclerView.setLayoutManager( new  LinearLayoutManager( this ));
         RecyclerAdapter recyclerAdapter =  new  RecyclerAdapter(createItemList());
         recyclerView.setAdapter(recyclerAdapter);
         recyclerView.setOnScrollListener( new  HidingScrollListener( this ));
     }
      
     private List<String> createItemList() {
         List<String> itemList =  new  ArrayList<>();
         for (int i=0;i<20;i++) {
             itemList.add( "Item " +i);
         }
         return  itemList;
     }
}

只是RecyclerViewToolbar基本的初始化操做,注意第27行OnScrollListener的設置。api

最有趣的部分是HidingScrollListener,讓咱們建立一個。app

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public abstract class HidingScrollListener extends RecyclerView.OnScrollListener {
  
     private int mToolbarOffset = 0;
     private int mToolbarHeight;
      
     public HidingScrollListener(Context context) {
         mToolbarHeight = Utils.getToolbarHeight(context);
     }
      
     @Override
     public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
         super .onScrolled(recyclerView, dx, dy);
          
         clipToolbarOffset();
         onMoved(mToolbarOffset);
          
         if ((mToolbarOffset <mToolbarHeight && dy>0) || (mToolbarOffset >0 && dy<0)) {
             mToolbarOffset += dy;
         }
     }
      
     private void clipToolbarOffset() {
         if (mToolbarOffset > mToolbarHeight) {
             mToolbarOffset = mToolbarHeight;
         else  if (mToolbarOffset < 0) {
             mToolbarOffset = 0;
         }
     }
      
     public abstract void onMoved(int distance);
}

若是你讀了前面一篇文章,這段代碼應該很眼熟(實際上此次還更簡單了)。這裏只有一個比較重要的變量-mToolbarOffset,它表示相對於Toolbar高度的滾動偏移量。爲了簡便起見,咱們只追蹤0到Toolbar高度之間的值:ide

1
2
3
if ((mToolbarOffset <mToolbarHeight && dy>0) || (mToolbarOffset >0 && dy<0)) {
     mToolbarOffset += dy;
}

當向上滾動的時候(注意在第一篇文章中咱們對於向上滾動的解釋)這個值將增長(可是咱們並不但願這個值大於Toolbar的高度),而向下滾動的時候這個值將減少(一樣,咱們也不但願減少到小於0),你很快會知道爲何咱們要做此限制的緣由。雖然上面的代碼已經有了限制,可是在很短的時間內(好比fling的時候),仍是有可能超過這個區間,所以須要調用clipToolbarOffset()方法來作二次限制,確保mToolbarOffset在0到Toolbar高度的範圍內,不然會出現抖動閃爍的狀況。咱們還定義了一個在scroll期間被調用的抽象方法onMoved()
佈局

讓咱們回到PartTwoActivity,同時實現scroll listener中的onMoved()方法:測試

1
2
3
4
5
6
7
8
9
10
11
12
13
private void initRecyclerView() {
     final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
     recyclerView.setLayoutManager( new  LinearLayoutManager( this ));
     RecyclerAdapter recyclerAdapter =  new  RecyclerAdapter(createItemList());
     recyclerView.setAdapter(recyclerAdapter);
      
     recyclerView.setOnScrollListener( new  HidingScrollListener( this ) {
         @Override
         public void onMoved(int distance) {
             mToolbarContainer.setTranslationY(-distance);
         }
     });
}

好了,咱們看看如今是什麼效果:

nosnap.gif


很是不錯,Toolbar隨着列表的滾動而滾動,而且能在消失以後再次隨着反向的滾動而滾回來,這和咱們的預期是一致的。這要歸功於咱們對mToolbarOffset的限制。若是咱們省略檢查mToolbarOffset是否大於0且小於mToolbarHeight,那麼當咱們向上滾動(這裏指手指向上,也許是做者疏忽吧,先後的意思不一致)時,Toolbar將會遠遠超出屏幕的範圍,想再次看到Toolbar須要等列表滾回到0的位置才行。而如今最多才滾動mToolbarHeight的距離,所以Toolbar始終緊挨着列表的最上面,所以向下滾動(這裏也是指手指向下)的時候,能當即看到Toolbar

雖然目前看起來還不錯,但並不是我想要的。若是在滾動一半的時候忽然中止,Toolbar將是部分可見的,這看起來很奇怪。實際上Google Play Games就是這種效果,但我以爲這是個bug。


讓Toolbar自動滾動到正確位置

正確是效果是Toolbar應該像Google Play store中那樣自動平滑的過分到該有的位置。


下面來看看新的HidingScrollListener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public abstract class HidingScrollListener extends RecyclerView.OnScrollListener {
      
     private static final float HIDE_THRESHOLD = 10;
     private static final float SHOW_THRESHOLD = 70;
      
     private int mToolbarOffset = 0;
     private boolean mControlsVisible =  true ;
     private int mToolbarHeight;
      
     public HidingScrollListener(Context context) {
         mToolbarHeight = Utils.getToolbarHeight(context);
     }
      
     @Override
     public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
         super .onScrollStateChanged(recyclerView, newState);
          
         if (newState == RecyclerView.SCROLL_STATE_IDLE) {
             if  (mControlsVisible) {
                 if  (mToolbarOffset > HIDE_THRESHOLD) {
                     setInvisible();
                 else  {
                     setVisible();
                 }
             else  {
                 if  ((mToolbarHeight - mToolbarOffset) > SHOW_THRESHOLD) {
                     setVisible();
                 else  {
                     setInvisible();
                 }
             }
         }
     }
      
     @Override
     public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
         super .onScrolled(recyclerView, dx, dy);
          
         clipToolbarOffset();
         onMoved(mToolbarOffset);
          
         if ((mToolbarOffset <mToolbarHeight && dy>0) || (mToolbarOffset >0 && dy<0)) {
             mToolbarOffset += dy;
         }
          
     }
      
     private void clipToolbarOffset() {
         if (mToolbarOffset > mToolbarHeight) {
             mToolbarOffset = mToolbarHeight;
         else  if (mToolbarOffset < 0) {
             mToolbarOffset = 0;
         }
     }
      
     private void setVisible() {
         if (mToolbarOffset > 0) {
             onShow();
             mToolbarOffset = 0;
         }
         mControlsVisible =  true ;
     }
      
     private void setInvisible() {
         if (mToolbarOffset < mToolbarHeight) {
             onHide();
             mToolbarOffset = mToolbarHeight;
         }
         mControlsVisible =  false ;
     }
      
     public abstract void onMoved(int distance);
     public abstract void onShow();
     public abstract void onHide();
}

比之前複雜了點,可是也不用怕,咱們只是重寫了RecyclerView.OnScrollListener的第二個方法onScrollStateChanged(),下面是onScrollStateChanged中所作的事情:

1.檢查列表是否處於RecyclerView.SCROLL_STATE_IDLE狀態,這個狀態下列表沒有滾動(由於若是在滾動,咱們是像之前同樣主動移動Toolbar的Y值)。

2.若是咱們放開了手指而且列表中止滾動(這是就是RecyclerView.SCROLL_STATE_IDLE狀態),咱們須要檢查當前Toolbar是否可見,若是是可見的,意味着在mToolbarOffset大於HIDE_THRESHOLD的時候隱藏它,而在mToolbarOffset小於SHOW_THRESHOLD的時候顯示它。

1
2
3
4
5
6
7
if  (mControlsVisible) {
     if  (mToolbarOffset > HIDE_THRESHOLD) {
         setInvisible();
     else  {
         setVisible();
     }
}

若是Toolbar是不可見的,咱們要作相反的事情-當mToolbarOffset(如今是從頂部計算因此是mToolbarHeight - mToolbarOffset)大於SHOW_THRESHOLD顯示,當小於IDE_THRESHOLD再次隱藏:

1
2
3
4
5
6
7
else  // it's not visible
     if  ((mToolbarHeight - mToolbarOffset) > SHOW_THRESHOLD) {
         setVisible();
     else  {
         setInvisible();
     }
}

ps:實話是說我沒看懂。。。

onScrolled()方法和以前保持一致,最後剩下的事情是在PartTwoActivity中實現兩個新的抽象方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void initRecyclerView() {
     final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
     recyclerView.setLayoutManager( new  LinearLayoutManager( this ));
     RecyclerAdapter recyclerAdapter =  new  RecyclerAdapter(createItemList());
     recyclerView.setAdapter(recyclerAdapter);
      
     recyclerView.setOnScrollListener( new  HidingScrollListener( this ) {
      
         @Override
         public void onMoved(int distance) {
             mToolbarContainer.setTranslationY(-distance);
         }
          
         @Override
         public void onShow() {
             mToolbarContainer.animate().translationY(0).setInterpolator( new  DecelerateInterpolator(2)).start();
         }
          
         @Override
         public void onHide() {
             mToolbarContainer.animate().translationY(-mToolbarHeight).setInterpolator( new  AccelerateInterpolator(2)).start();
         }
          
     });
}


如今來看看編譯運行的結果:

Snap no tabs gif

看起來還比較順利!

等等,不是說要作成play store的效果嗎,還差了個tab吧,你可能會以爲添加tab會讓事情變得複雜不少,讓我來告訴你。其實不是那麼回事。

添加tab

須要修改Activity的佈局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android"
     android:layout_width= "match_parent"
     android:layout_height= "match_parent" >
  
     <android.support.v7.widget.RecyclerView
         android:id= "@+id/recyclerView"
         android:layout_width= "match_parent"
         android:layout_height= "match_parent"
         android:clipToPadding= "false" />
  
     <LinearLayout
         android:id= "@+id/toolbarContainer"
         android:layout_width= "match_parent"
         android:layout_height= "wrap_content"
         android:orientation= "vertical" >
      
         <android.support.v7.widget.Toolbar
             android:id= "@+id/toolbar"
             android:layout_width= "match_parent"
             android:layout_height= "?attr/actionBarSize"
             android:background= "?attr/colorPrimary" />
      
         <include layout= "@layout/tabs" />
     </LinearLayout>
  
</FrameLayout>

其中tabs.xml代碼以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android"
     android:layout_width= "match_parent"
     android:layout_height= "@dimen/tabsHeight"
     android:background= "?attr/colorPrimary" >
     <FrameLayout
         android:layout_width= "0dp"
         android:layout_height= "match_parent"
         android:layout_weight= "1"  >
         <TextView
             android:layout_width= "match_parent"
             android:layout_height= "match_parent"
             android:text= "@string/tab_1"
             android:gravity= "center"
             style= "@style/Base.TextAppearance.AppCompat.Body2"
             android:textColor= "@android:color/white"
             android:background= "@android:color/transparent"  />
         <View
             android:layout_width= "match_parent"
             android:layout_height= "6dp"
             android:layout_gravity= "bottom"
             android:background= "@android:color/white"  />
     </FrameLayout>
     <TextView
         android:layout_width= "0dp"
         android:layout_height= "match_parent"
         android:layout_weight= "1"
         android:text= "@string/tab_2"
         android:gravity= "center"
         style= "@style/Base.TextAppearance.AppCompat.Body2"
         android:textColor= "@android:color/white"
         android:background= "@android:color/transparent"  />
</LinearLayout>

能夠看到,我並無添加一個真正意義上的Tab,而是一個長得像tab的佈局。但這並不會改變什麼,這裏能夠是任意的view,原理都是同樣的,至於材料設計風格的tab,github上有一些實現,你能夠用來替換。

添加Tab意味着列表再次被擋住一部分空間,所以須要增長padding的值。考慮到靈活性,此次咱們再也不使用xml來設置padding,而是在代碼中設置:

1
2
3
4
5
6
7
8
9
private void initRecyclerView() {
     final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
      
     int paddingTop = Utils.getToolbarHeight(PartTwoActivity. this ) + Utils.getTabsHeight(PartTwoActivity. this );
     recyclerView.setPadding(recyclerView.getPaddingLeft(), paddingTop, recyclerView.getPaddingRight(), recyclerView.getPaddingBottom());
      
     recyclerView.setLayoutManager( new  LinearLayoutManager( this ));
     // ...
}

很簡單,咱們將padding設置成ToolbarTab高度之和,運行看看正確與否:

Screen with tabs

看來是正確的列表的第一個item能夠徹底顯示,那麼咱們繼續,實際上,HidingScrollListener類中的代碼徹底不變,只需呀變動下PartTwoActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class PartTwoActivity extends ActionBarActivity {
      
     private LinearLayout mToolbarContainer;
     private int mToolbarHeight;
      
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         setTheme(R.style.AppThemeGreen);
         super .onCreate(savedInstanceState);
         setContentView(R.layout.activity_part_two);
          
         mToolbarContainer = (LinearLayout) findViewById(R.id.toolbarContainer);
         initToolbar();
         initRecyclerView();
     }
      
     private void initToolbar() {
         Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar);
         setSupportActionBar(mToolbar);
         setTitle(getString(R.string.app_name));
         mToolbar.setTitleTextColor(getResources().getColor(android.R.color.white));
         mToolbarHeight = Utils.getToolbarHeight( this );
     }
      
     private void initRecyclerView() {
         final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
          
         int paddingTop = Utils.getToolbarHeight(PartTwoActivity.
相關文章
相關標籤/搜索