CSDN博客地址:http://blog.csdn.net/zhangjinhuang/article/details/52015020
由于公司的项目基本上都是以ios为主,android为辅的,因此开需求会议的时候,经常会碰到“要实现点击状态栏回到顶部”的需求,这个功能ios实现起来特简单,也就一个属性的问题,但是android实现起来却超麻烦(所以当初不知道是基于什么原因,老大基本上都是把这个功能推掉的),最近发现微信朋友圈也具有类似ios点击状态栏回到顶部的功能,也许你们会说我肯定是点错了,点到标题栏或者直接认为我状态栏、标题栏不分,但是,我可以在这里很认真、很负责地告诉大家我没点错也没有状态栏、标题栏都不分,只不过点击状态栏回到顶部这个功能貌似是跟手机系统有关,我在坚果(5.0)手机上试了下有这个功能,在红米1s(4.4.2)和联想手机上都不行,但是这丝毫不影响我的求知欲望,因为这篇文章或许是第一篇讲解如何实现类似ios点击状态栏回到顶部的技术文章。
一开始的时候,我以为很简单,因为先前接触沉浸式状态栏的时候知道可以给状态栏添加一个View,然后再通过给这个View添加一系列的属性或者事件什么的,但是真正动手实现起来的时候却发现了一个很神奇的问题,在对该View添加背影色,添加文字内容什么的都能显示出来,但是我一在状态栏上下拉时,那个View就不见了,当我再点击内容区域时,那个View又出来了,当时我的内心是十分崩溃的,于是在经过一系列的检查和尝试依旧没有找到问题所在后,果断采用了另一种方法就是通过给状态栏添加一个悬浮窗,对,没错就是悬浮窗,于是乎在确定方案后就翻阅起API文档来,因为要想把悬浮窗放在状态栏上面就得给Window设置一系列的属性,另外需要注意的是,在小米手机上需要自己手动在安全中心上打开悬浮窗的权限,如果其它手机没出现悬浮窗的话估计也是权限的问题,在相应地方打开权限就好,实现效果如下所示:
要想实现一个悬浮窗效果需要借助WindowManager类,该WindowManager提供了3个用来操作视图的类,addView(View view,LayoutParams params),updateViewLayout(View view,LayoutParams params),及removeView(View view),其作用如其方法名一样分别是用来增加、更新及移除View。其次,我们还需要借助WindowManager的LayoutParams来为悬浮窗添加一系列的属性,如type、flags、format等等。
1、可以通过如下代码获取WindowManager:
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
2、获取WindowManager的LayoutParams并设置一系列属性:
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
//设置window type 下面变量2002是在屏幕区域显示,2003则可以显示在状态栏之上
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
//设置图片格式,效果为背景透明
params.format = PixelFormat.RGBA_8888;
//设置可以显示在状态栏上,flags值须大于1280时,悬浮窗才会在状态栏之上
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR |
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
//设置悬浮窗口长宽数据
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.MATCH_PARENT;
// 悬浮窗默认显示以左上角为起始坐标
params.gravity = Gravity.LEFT | Gravity.TOP;
3、设置完属性后就可以加载悬浮窗需要显示的内容了,如下:
View view = LayoutInflater.from(this).inflate(R.layout.view_window, null);
//获取子控件
TextView tv_statusBarView = (TextView) view.findViewById(R.id.tv_statusBarView);
//动态将子控件的高度设置成状态栏的高度
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) tv_statusBarView.getLayoutParams();
layoutParams.height = StatusBarUtils.getStatusBarHeight(getApplicationContext());
tv_statusBarView.setLayoutParams(layoutParams);
此处加载的view_window布局文件的代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00000000"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical">
<TextView
android:id="@+id/tv_statusBarView"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#00000000"
android:clickable="true"
android:enabled="true"
android:gravity="center"/>
</LinearLayout>
获取状态栏高度的getStatusBarHeight方法如下所示:
/**
* 获得状态栏高度
*/
public static int getStatusBarHeight(Context context) {
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
return context.getResources().getDimensionPixelSize(resourceId);
}
最后需要把加载的View通过WindowManager的addView方法添加到状态栏上:
windowManager.addView(view, params);
此时,悬浮窗就已经被添加在状态栏上了,但是此时点击状态栏是没有任何响应的,因此悬浮窗位于状态栏上并且把状态栏的一系列事件都给拦截了,因此,需要我们为悬浮窗添加一个触摸事件,也许大家会问,为什么是触摸事件而不是点击事件呢?因为如下上面所说悬浮窗把状态栏的事件都给拦截了,因此需要在触摸事件中计算滑动的距离来判断是点击悬浮窗呢还是让状态栏展开呢,这里的让状态栏展开是通过反射的方式来操作的,下面会讲到,另外我们还需要借助GestureDetector来进行手势操作。
1、首先,需要定义一个手势监听器CustomOnGustureListener并让其继承自SimpleOnGestureListener并实现其中的onSingleTapConfirmed方法,代码如下所示:此时,悬浮窗就已经被添加在状态栏上了,但是此时点击状态栏是没有任何响应的,因此悬浮窗位于状态栏上并且把状态栏的一系列事件都给拦截了,因此,需要我们为悬浮窗添加一个触摸事件,也许大家会问,为什么是触摸事件而不是点击事件呢?因为如下上面所说悬浮窗把状态栏的事件都给拦截了,因此需要在触摸事件中计算滑动的距离来判断是点击悬浮窗呢还是让状态栏展开呢,这里的让状态栏展开是通过反射的方式来操作的,下面会讲到,另外我们还需要借助GestureDetector来进行手势操作。
/**
* 自定义手势监听器
*/
private class CustomOnGustureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
//如果isMove不为true表示是点击事件
if (!isMove) {
Toast.makeText(getApplicationContext(), "你点击了悬浮窗", Toast.LENGTH_SHORT).show();
Log.i("test", "onClick");
if(onStatusBarClickListener!=null){
onStatusBarClickListener.onClick();
}
}
return super.onSingleTapConfirmed(e);
}
}
2、其次,还需要定义一个OnFloatingListener类让其实现OnTouchListener接口,然后覆写其onTouchEvent方法,最后并将其交给GetstureDetector处理,代码如下所示:
//分别用于记录按下,移动、抬起时相应的x、y坐标
private int startX, startY, moveX, moveY, stopX, stopY;
private int offsetX, offsetY;
//用于标记悬浮窗是否有移动
private boolean isMove;
/**
* 由于悬浮窗是位于状态栏之上且覆盖状态栏的焦点以至于状态栏的相应事件失效,如:下拉出通知
* 因此需要通过监听悬浮窗在不同状态下触发相应的事件
*/
private class OnFloatingListener implements View.OnTouchListener {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
isMove = false;
startX = (int) event.getX();
startY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
moveX = (int) event.getX();
moveY = (int) event.getY();
offsetY = Math.abs(startY - moveY);
//当移动距离大于某个值时,表示是在下拉状态栏,此时展开状态栏
if (Math.abs(offsetY) >= 8) {
StatusBarUtils.expandStatusBar(getApplicationContext());
}
break;
case MotionEvent.ACTION_UP:
stopX = (int) event.getX();
stopY = (int) event.getY();
offsetY = Math.abs(startY - stopY);
//如果手抬起时移动的距离大于某个值,表示是处于下拉操作
if (Math.abs(offsetY) >= 8) {
isMove = true;
}
break;
}
return gestureDetector.onTouchEvent(event);//将onTouchEvent交给GestureDetector处理
}
}
最后就是初始化GestureDetector和绑定触摸事件了,代码如下所示:
GestureDetector gestureDetector = new GestureDetector(this, new CustomOnGustureListener());
tv_statusBarView.setOnTouchListener(new OnFloatingListener());
当然为了理方便的使用,我将上面所有代码都放在一个Service中,并且还为其提供了一个当点击状态栏时的回调,代码如下所示:
private OnStatusBarClickListener onStatusBarClickListener;
public void setOnStatusBarClickListener(OnStatusBarClickListener onStatusBarClickListener) {
this.onStatusBarClickListener = onStatusBarClickListener;
}
public interface OnStatusBarClickListener{
void onClick();
}
此时,关于如何实现类似ios点击状态栏回到顶部的主要功能就已经全部实现了,为了更方便的使用,我将启动悬浮窗的代码的放在BaseActivity中,代码如下所示:
package abner.clickstatusbar2top.activities;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Window;
import abner.clickstatusbar2top.service.FloatingService;
/**
* Created by abner on 2016/7/24.
*/
public abstract class BaseActivity extends AppCompatActivity {
private Intent intent;
protected FloatingService floatingService;
private ServiceConnection connection;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
// initServiceConnection();
startFloatingService();
// setListener();
}
private void initServiceConnection() {
connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i("test", "onServiceConnected");
floatingService = ((FloatingService.SubFloatingService) service).getService();
if (floatingService != null) {
setListener();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
}
protected abstract void setListener();
@Override
protected void onPause() {
super.onPause();
stopFloatingService();
}
private void startFloatingService() {
intent = new Intent(this, FloatingService.class);
// startService(intent);
if (connection == null) {
initServiceConnection();
}
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
private void stopFloatingService() {
if (connection != null) {
// stopService(intent);
unbindService(connection);
connection = null;
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
startFloatingService();
} else {
stopFloatingService();
}
}
}
在代码的最后可以发现,添加了一个onWindowFocusChanged方法,主要是为了解决当下拉状态栏时,再点击状态栏时也会响应点击事件,由于下拉状态栏时当前Activity处于不可见状态,因此可以通过onWindowFocusChanged方法进行判断。
在使用时,我们只需要将我们的Activity都继承自BaseActivity,然后在setListener方法中调用setOnStatusBarClickListener方法并在其回调中让列表回到顶部即可实现点击状态栏回到顶部功能,如:
package abner.clickstatusbar2top.activities;
import android.os.Bundle;
import android.util.Log;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.List;
import abner.clickstatusbar2top.R;
import abner.clickstatusbar2top.adapter.NewsAdapter;
import abner.clickstatusbar2top.bean.News;
import abner.clickstatusbar2top.service.FloatingService;
public class MainActivity extends BaseActivity {
private ListView lv_content;
private List datas;
private NewsAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
lv_content = (ListView) findViewById(R.id.lv_content);
initData();
}
@Override
protected void setListener() {
floatingService.setOnStatusBarClickListener(new FloatingService.OnStatusBarClickListener() {
@Override
public void onClick() {
Log.i("test","setListener");
lv_content.smoothScrollToPosition(0);
}
});
}
private void initData() {
datas = new ArrayList<>();
News news;
for (int i = 0; i < 50; i++) {
news = new News();
news.setTitle("这是标题:"+i);
news.setDesc("这是描述信息:"+i);
news.setDate("这是时间"+i);
datas.add(news);
}
adapter = new NewsAdapter(this,datas);
lv_content.setAdapter(adapter);
}
}