Android 实现导航 Tab 悬浮

“饿了么”导航Tab栏悬浮的效果图。

这里填写图片的描述

可以看到上图中的“分类”、“排序”、“筛选”会悬浮在app的顶部,状态随着ScrollView(也可能不是ScrollView,在这里姑且把这滑动的UI控件当作ScrollView吧)的滚动而变化。像这种导航Tab栏悬浮的作用相信大家都能体会到,Tab栏不会随着ScrollView等的滚动而被滑出屏幕外,增加了与用户之间的交互性和方便性。

看到上面的效果,相信大家都跃跃欲试了,那就让我们开始吧。

好了,根据上面的就得到了对ScrollView滑动的监听了。接下来要思考的问题就是如何让Tab栏实现悬浮的效果呢?这里给出的方法有两种,第一种就是使用WindowManager来动态地添加一个View悬浮在顶部;第二种就是随着ScrollView的滑动不断重新设置Tab栏的布局位置。

我们先来看看第一种实现方法,首先是xml布局了。

Activity的布局,activity_main.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
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<RelativeLayout
android:id="@+id/rl_title"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimary">

<ImageView
android:id="@+id/iv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:src="@drawable/new_img_back" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/app_name"
android:textColor="@android:color/white"
android:textSize="18sp" />

</RelativeLayout>

<com.yuqirong.tabsuspenddemo.view.MyScrollView
android:id="@+id/mScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#cccccc"
android:orientation="vertical">

<ImageView
android:id="@+id/iv_pic"
android:layout_width="match_parent"
android:layout_height="180dp"
android:scaleType="centerCrop"
android:src="@drawable/ic_bg_personal_page" />

<include layout="@layout/tab_layout" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginBottom="5dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="15dp"
android:background="@android:color/white"
android:orientation="horizontal">

</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginBottom="5dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="15dp"
android:background="@android:color/white"
android:orientation="horizontal">

</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginBottom="5dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="15dp"
android:background="@android:color/white"
android:orientation="horizontal">

</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginBottom="5dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="15dp"
android:background="@android:color/white"
android:orientation="horizontal">

</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginBottom="5dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="15dp"
android:background="@android:color/white"
android:orientation="horizontal">

</LinearLayout>


<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginBottom="5dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="15dp"
android:background="@android:color/white"
android:orientation="horizontal">

</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginBottom="5dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="15dp"
android:background="@android:color/white"
android:orientation="horizontal">

</LinearLayout>

</LinearLayout>
</com.yuqirong.tabsuspenddemo.view.MyScrollView>
</LinearLayout>

Tab栏的布局,tab_layout.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
33
34
35
36
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_tab"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@color/colorPrimary"
android:orientation="horizontal">

<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="分类"
android:textColor="@android:color/white"
android:textSize="18sp" />

<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="排序"
android:textColor="@android:color/white"
android:textSize="18sp" />

<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="筛选"
android:textColor="@android:color/white"
android:textSize="18sp" />

</LinearLayout>

上面布局中的很多空白LinearLayout主要是拉长ScrollView,效果图就是这样的:

这里填写图片的描述

然后我们来看看onCreate(Bundle savedInstanceState)

1
2
3
4
5
6
7
8
9
10
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().hide();
setContentView(R.layout.activity_main);
mScrollView = (MyScrollView) findViewById(R.id.mScrollView);
mScrollView.setOnScrollListener(this);
ll_tab = (LinearLayout) findViewById(R.id.ll_tab);
windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
}

我们先在onCreate(Bundle savedInstanceState)中给ScrollView添加了滑动距离监听器以及得到了一个windowManager的对象。还有一点需要注意的是:我们调用了getSupportActionBar().hide();去掉了标题栏(MainActivity继承了AppCompatActivity)。这是因为标题栏的存在导致了在计算悬浮窗y轴的值时要额外加上标题栏的高度(当然你也可以保留标题栏,然后计算时再加上标题栏的高度^_^!)。

然后在onWindowFocusChanged(boolean hasFocus)得到Tab栏的高度、getTop()值等,以便下面备用。

1
2
3
4
5
6
7
8
9
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
tabHeight = ll_tab.getHeight();
tabTop = ll_tab.getTop();
scrollTop = mScrollView.getTop();
}
}

之后在滑动监听器的回调方法onScroll(int scrollY)中来控制显示还是隐藏悬浮窗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void onScroll(int scrollY) {
Log.i(TAG, "scrollY = " + scrollY + ", tabTop = " + tabTop);
if (scrollY > tabTop) {
// 如果没显示
if (!isShowWindow) {
showWindow();
}
} else {
// 如果显示了
if (isShowWindow) {
removeWindow();
}
}
}

上面的代码比较简单,不用我过多叙述了。下面是removeWindow()、showWindow()两个方法:

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
// 显示window
private void removeWindow() {
if (ll_tab_temp != null)
windowManager.removeView(ll_tab_temp);
isShowWindow = false;
}

// 移除window
private void showWindow() {
if (ll_tab_temp == null) {
ll_tab_temp = LayoutInflater.from(this).inflate(R.layout.tab_layout, null);
}
if (layoutParams == null) {
layoutParams = new WindowManager.LayoutParams();
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE; //悬浮窗的类型,一般设为2002,表示在所有应用程序之上,但在状态栏之下
layoutParams.format = PixelFormat.RGBA_8888;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; //悬浮窗的行为,比如说不可聚焦,非模态对话框等等
layoutParams.gravity = Gravity.TOP; //悬浮窗的对齐方式
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
layoutParams.height = tabHeight;
layoutParams.x = 0; //悬浮窗X的位置
layoutParams.y = scrollTop; //悬浮窗Y的位置
}
windowManager.addView(ll_tab_temp, layoutParams);
isShowWindow = true;
}

这两个方法也很简单,而且有注释,相信大家可以看懂。

最后,不要忘了在AndroidManifest.xml里申请显示悬浮窗的权限:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

到这里,整体的代码就这些了。一起来看看效果吧:

这里填写图片的描述

但是用这种方法来实现Tab栏悬浮功能有一个缺点,那就是如果该app没有被赋予显示悬浮窗的权限,那么该功能就变成鸡肋了。当然还有第二种方法来实现,不过只能等到下一篇博文再讲了。