无意间了解到沉浸式状态栏,感觉贼拉的高大上,于是就是试着去了解一下,就有了这篇文章。下面就来了解一下啥叫沉浸式状态栏。传统的手机状态栏是呈现出黑色条状的,有的和手机主界面有很明显的区别。这一样就在一定程度上牺牲了视觉宽度,界面面积变小。Google从android kitkat(Android 4.4)开始,给我们开发者提供了一套能透明的系统ui样式给状态栏和导航栏,这样的话就不用向以前那样每天面对着黑乎乎的上下两条黑栏了,还可以调成跟Activity一样的样式,形成一个完整的主题,和IOS7.0以上系统一样了,沉浸式状态栏和主界面颜色和谐一体,视觉效果更加炫酷。不过虽然听上去好像是很高大上的沉浸式效果,实际看上去貌似就是将内容全屏化了而已嘛。其实这算是一个争议点了。不少人纠结于沉浸式状态栏到底是将屏幕显示内容扩大还是仅仅是改变状态栏、标题栏的颜色。其实我更倾向于后者。在4.4之前状态栏一直是黑色的,在4.4中带来了 windowTranslucentStatus 这一特性,因此可以实现给状态栏设置颜色,视觉上的效果,感觉容器部分和状态栏、标题栏融为一体,更加直接的说就是改变状态栏、标题栏的颜色,当时可以根据界面颜色改变状态栏、标题栏的颜色实现跟加完整的界面显示,这应该是沉浸式状态栏受追捧的原因吧。
谷歌并没有给出沉浸式状态栏这个概念,谷歌只说了沉浸式模式(Immersive Mode)。不过沉浸式状态栏这个名字其实挺不错,只能随大众,但是Android的环境并没有IOS环境一样特别统一,比如华为rom的跟小米rom的虚拟按键完全不一样,并且安卓版本众多涉及到版本兼容问题,所有Android开发者不容易。这点在沉浸式状态栏的开发中显得尤为重要。如果你在4.4之前的机子上显示沉浸式状态栏的话,经常出现一些意想不到的结果。沉浸式是APP界面图片延伸到状态栏, 应用本身沉浸于状态栏,所以如果第三方的软件没有为状态栏分配图片,那么自然就是黑色。顶端的状态栏和下面的虚拟按键都隐藏,需要的时候从边缘划出。沉浸模式。当启用该模式,应用程序的界面将占据整个屏幕,系统自动将隐藏系统的状态栏和导航栏,让应用程序内容可以在最大显示范围呈现,增加大屏体验,而当需要查看通知的时候只需要从顶部向下滑动就能呼出通知栏。沉浸模式实际上有两种:一种叫“沉浸模式”,状态栏和虚拟按钮会自动隐藏、应用自动全屏,这种模式下,应用占据屏幕的全部空间, 只有当用户从屏幕的上方边沿处向下划动时, 才会退出沉浸模式, 用户触摸屏幕其它部分时, 不会退出该模式, 这种模式比较适用于阅读器、 杂志类应用。另外一种叫“黏性沉浸模式”,让状态栏和虚拟按钮半透明,应用使用屏幕的全部空间, 当用户从屏幕的上方边沿处向下滑动时,也不会退出该模式, 但是系统界面 (状态栏、 导航栏) 将会以半透明的效果浮现在应用视图之上 , 只有当用户点击系统界面上的控件时, 才会退出黏性沉浸模式。
下面来说一说具体的实现。一个Android应用程序的界面上其实是有很多系统元素的,有状态栏、ActionBar、导航栏等。而打造沉浸式模式的用户体验,就是要将这些系统元素进行整合,当主界面改变时,状态栏、ActionBar、导航栏同时也发生改变。这里先调用getWindow().getDecorView()方法获取到了当前界面的DecorView,然后调用它的setSystemUiVisibility()方法来设置系统UI元素的可见性。其中,SYSTEM_UI_FLAG_FULLSCREEN表示全屏的意思,也就是会将状态栏隐藏。另外,根据Android的设计建议,ActionBar是不应该独立于状态栏而单独显示的,因此状态栏如果隐藏了,我们同时也需要调用ActionBar的hide()方法将ActionBar也进行隐藏这种效果不叫沉浸式状态栏,也完全没有沉浸式状态栏这种说法,我们估且可以把它叫做透明状态栏效果吧。
隐藏状态栏:
setContentView(R.layout.activity_main); //再该方法后执行 if (Build.VERSION.SDK_INT >= 21) { View decorView =getWindow().getDecorView(); int option =View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |View.SYSTEM_UI_FLAG_LAYOUT_STABLE; decorView.setSystemUiVisibility(option); getWindow().setStatusBarColor(Color.TRANSPARENT); } ActionBar actionBar =getSupportActionBar(); actionBar.hide();
具体的沉浸效果该如何实现呢,系统提供实现沉浸式状态栏的方法,通过WindowManager来实现,可分为两步:
1. 在需要实现沉浸式状态栏的Activity的布局中添加以下参数
android:fitsSystemWindows="true"
android:clipToPadding="true"
2. 在Activity的setContentView()方法后面调用初始化的方法即可。
private voidinitState() { if (Build.VERSION.SDK_INT >=Build.VERSION_CODES.KITKAT) { //透明状态栏 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); //透明导航栏 getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); } }
当上述的实现效果,其实并不好,没有在布局中设置clipToPadding为true的时候,会对应用的顶部Toolbar进行拉伸,在布局中两个参数都进行设置后,顶部状态栏变成了白色。这样,我在github上找到一个很好的沉浸状态栏效果,来看一下。
首先添加依赖,导入下面的包。有时候可能会出现版本不统一的问题,依次保证联网的情况下点击一下同步android studio会自动下载包。
compile 'com.jaeger.statusbaruitl:library:1.2.5'
在自定义控件中实现的进本逻辑,代码较长。
packagecom.xiaoyuan; importandroid.content.Context; importandroid.content.res.TypedArray; importandroid.graphics.Canvas; importandroid.graphics.drawable.Drawable; importandroid.os.Build; importandroid.util.AttributeSet; importandroid.view.MotionEvent; importandroid.view.View; importandroid.view.ViewGroup; importandroid.view.animation.AlphaAnimation; importandroid.widget.ScrollView; importjava.util.ArrayList; /*** @authorEmil Sj�lander - sjolander.emil@gmail.com */ public class StickyScrollView extendsScrollView { /*** Tag for views that should stick and have constant drawing. e.g. * TextViews, ImageViews etc */ public static final String STICKY_TAG = "sticky"; /*** Flag for views that should stick and have non-constant drawing. e.g. * Buttons, ProgressBars etc */ public static final String FLAG_NONCONSTANT = "-nonconstant"; /*** Flag for views that have aren't fully opaque */ public static final String FLAG_HASTRANSPARANCY = "-hastransparancy"; /*** Default height of the shadow peeking out below the stuck view. */ private static final int DEFAULT_SHADOW_HEIGHT = 10; //dp; /*** XKJ add for add 50dp offset of top */ private static int MIN_STICK_TOP = 100;//px //private static final int MIN_STICK_TOP = 0; private ArrayList<View>stickyViews; privateView currentlyStickingView; private floatstickyViewTopOffset; private intstickyViewLeftOffset; private booleanredirectTouchesToStickyView; private booleanclippingToPadding; private booleanclipToPaddingHasBeenSet; private intmShadowHeight; privateDrawable mShadowDrawable; private OnScrollChangedListener mOnScrollHandler = null; private IOnScrollToEnd mOnScrollToEnd = null; private final Runnable invalidateRunnable = newRunnable() { @Override public voidrun() { if (currentlyStickingView != null) { int l =getLeftForViewRelativeOnlyChild(currentlyStickingView); int t =getBottomForViewRelativeOnlyChild(currentlyStickingView); int r =getRightForViewRelativeOnlyChild(currentlyStickingView); int b = (int) (getScrollY() + (currentlyStickingView.getHeight() +stickyViewTopOffset)); invalidate(l, t, r, b); } postDelayed(this, 16); } }; publicStickyScrollView(Context context) { this(context, null); } publicStickyScrollView(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.scrollViewStyle); } public StickyScrollView(Context context, AttributeSet attrs, intdefStyle) { super(context, attrs, defStyle); setup(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StickyScrollView, defStyle, 0); final float density =context.getResources().getDisplayMetrics().density; int defaultShadowHeightInPix = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f); mShadowHeight =a.getDimensionPixelSize(R.styleable.StickyScrollView_stuckShadowHeight, defaultShadowHeightInPix); int shadowDrawableRes = a.getResourceId(R.styleable.StickyScrollView_stuckShadowDrawable, -1); if (shadowDrawableRes != -1) { mShadowDrawable =context.getResources().getDrawable(shadowDrawableRes); } a.recycle(); } /*** Sets the height of the shadow drawable in pixels. * * @paramheight */ public void setShadowHeight(intheight) { mShadowHeight =height; } public voidsetup() { stickyViews = new ArrayList<View>(); } private intgetLeftForViewRelativeOnlyChild(View v) { int left =v.getLeft(); while (v.getParent() != getChildAt(0)) { v =(View) v.getParent(); left +=v.getLeft(); } returnleft; } private intgetTopForViewRelativeOnlyChild(View v) { int top =v.getTop(); while (v.getParent() != getChildAt(0)) { v =(View) v.getParent(); top +=v.getTop(); } returntop; } private intgetRightForViewRelativeOnlyChild(View v) { int right =v.getRight(); while (v.getParent() != getChildAt(0)) { v =(View) v.getParent(); right +=v.getRight(); } returnright; } private intgetBottomForViewRelativeOnlyChild(View v) { int bottom =v.getBottom(); while (v.getParent() != getChildAt(0)) { v =(View) v.getParent(); bottom +=v.getBottom(); } returnbottom; } @Override protected void onLayout(boolean changed, int l, int t, int r, intb) { super.onLayout(changed, l, t, r, b); if (!clipToPaddingHasBeenSet) { clippingToPadding = true; } notifyHierarchyChanged(); } @Override public void setClipToPadding(booleanclipToPadding) { super.setClipToPadding(clipToPadding); clippingToPadding =clipToPadding; clipToPaddingHasBeenSet = true; } @Override public voidaddView(View child) { super.addView(child); findStickyViews(child); } @Override public void addView(View child, intindex) { super.addView(child, index); findStickyViews(child); } @Override public void addView(View child, intindex, android.view.ViewGroup.LayoutParams params) { super.addView(child, index, params); findStickyViews(child); } @Override public void addView(View child, int width, intheight) { super.addView(child, width, height); findStickyViews(child); } @Override public voidaddView(View child, android.view.ViewGroup.LayoutParams params) { super.addView(child, params); findStickyViews(child); } @Override protected voiddispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (currentlyStickingView != null) { canvas.save(); canvas.translate(getPaddingLeft() + stickyViewLeftOffset, getScrollY() +stickyViewTopOffset + (clippingToPadding ? getPaddingTop() : 0)); canvas.clipRect(0, (clippingToPadding ? -stickyViewTopOffset : 0), getWidth() -stickyViewLeftOffset, currentlyStickingView.getHeight() + mShadowHeight + 1); if (mShadowDrawable != null) { int left = 0; int right =currentlyStickingView.getWidth(); int top =currentlyStickingView.getHeight(); int bottom = currentlyStickingView.getHeight() +mShadowHeight; mShadowDrawable.setBounds(left, top, right, bottom); mShadowDrawable.draw(canvas); } canvas.clipRect(0, (clippingToPadding ? -stickyViewTopOffset : 0), getWidth(), currentlyStickingView.getHeight()); if(getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) { showView(currentlyStickingView); currentlyStickingView.draw(canvas); hideView(currentlyStickingView); } else{ currentlyStickingView.draw(canvas); } canvas.restore(); } } @Override public booleandispatchTouchEvent(MotionEvent ev) { if (ev.getAction() ==MotionEvent.ACTION_DOWN) { redirectTouchesToStickyView = true; } if(redirectTouchesToStickyView) { redirectTouchesToStickyView = currentlyStickingView != null; if(redirectTouchesToStickyView) { redirectTouchesToStickyView = ev.getY() <= (currentlyStickingView.getHeight() +stickyViewTopOffset) && ev.getX() >=getLeftForViewRelativeOnlyChild(currentlyStickingView) && ev.getX() <=getRightForViewRelativeOnlyChild(currentlyStickingView); } } else if (currentlyStickingView == null) { redirectTouchesToStickyView = false; } if(redirectTouchesToStickyView) { ev.offsetLocation(0, -1 * ((getScrollY() + stickyViewTopOffset) -getTopForViewRelativeOnlyChild(currentlyStickingView))); //XKJ add TODO: remove this currentlyStickingView.invalidate(); } return super.dispatchTouchEvent(ev); } private boolean hasNotDoneActionDown = true; @Override public booleanonTouchEvent(MotionEvent ev) { if(redirectTouchesToStickyView) { ev.offsetLocation(0, ((getScrollY() + stickyViewTopOffset) -getTopForViewRelativeOnlyChild(currentlyStickingView))); } if (ev.getAction() ==MotionEvent.ACTION_DOWN) { hasNotDoneActionDown = false; } if(hasNotDoneActionDown) { MotionEvent down =MotionEvent.obtain(ev); down.setAction(MotionEvent.ACTION_DOWN); super.onTouchEvent(down); hasNotDoneActionDown = false; } if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() ==MotionEvent.ACTION_CANCEL) { hasNotDoneActionDown = true; } return super.onTouchEvent(ev); } @Override protected void onScrollChanged(int l, int t, int oldl, intoldt) { super.onScrollChanged(l, t, oldl, oldt); doTheStickyThing(); if (mOnScrollHandler != null) { mOnScrollHandler.onScrollChanged(l, t, oldl, oldt); } int maxScroll = getChildAt(0).getHeight() -getHeight(); if (getChildCount() > 0 && t ==maxScroll) { if (mOnScrollToEnd != null) { mOnScrollToEnd.onScrollToEnd(); } } } public voidsetOnScrollListener(OnScrollChangedListener handler) { mOnScrollHandler =handler; } public interfaceOnScrollChangedListener { public void onScrollChanged(int l, int t, int oldl, intoldt); } public interfaceIOnScrollToEnd { public voidonScrollToEnd(); } public voidsetOnScrollToEndListener(IOnScrollToEnd handler) { mOnScrollToEnd =handler; } private voiddoTheStickyThing() { View viewThatShouldStick = null; View approachingView = null; for(View v : stickyViews) { int viewTop = getTopForViewRelativeOnlyChild(v) - getScrollY() + (clippingToPadding ? 0: getPaddingTop()) - MIN_STICK_TOP;//add 50dp if (viewTop <= 0) { if (viewThatShouldStick == null || viewTop > (getTopForViewRelativeOnlyChild(viewThatShouldStick) - getScrollY() + (clippingToPadding ? 0: getPaddingTop()))) { viewThatShouldStick =v; } } else{ if (approachingView == null || viewTop < (getTopForViewRelativeOnlyChild(approachingView) - getScrollY() + (clippingToPadding ? 0: getPaddingTop()))) { approachingView =v; } } } if (viewThatShouldStick != null) { stickyViewTopOffset = approachingView == null ?MIN_STICK_TOP : Math.min(MIN_STICK_TOP, getTopForViewRelativeOnlyChild(approachingView) -getScrollY() + (clippingToPadding ? 0 : getPaddingTop()) -viewThatShouldStick.getHeight()); if (viewThatShouldStick !=currentlyStickingView) { if (currentlyStickingView != null) { stopStickingCurrentlyStickingView(); } //only compute the left offset when we start sticking. stickyViewLeftOffset =getLeftForViewRelativeOnlyChild(viewThatShouldStick); startStickingView(viewThatShouldStick); } } else if (currentlyStickingView != null) { stopStickingCurrentlyStickingView(); } } private voidstartStickingView(View viewThatShouldStick) { currentlyStickingView =viewThatShouldStick; if(getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) { hideView(currentlyStickingView); } if(((String) currentlyStickingView.getTag()).contains(FLAG_NONCONSTANT)) { post(invalidateRunnable); } } private voidstopStickingCurrentlyStickingView() { if(getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) { showView(currentlyStickingView); } currentlyStickingView = null; removeCallbacks(invalidateRunnable); } /*** Notify that the sticky attribute has been added or removed from one or * more views in the View hierarchy */ public voidnotifyStickyAttributeChanged() { notifyHierarchyChanged(); } private voidnotifyHierarchyChanged() { if (currentlyStickingView != null) { stopStickingCurrentlyStickingView(); } stickyViews.clear(); findStickyViews(getChildAt(0)); doTheStickyThing(); invalidate(); } private voidfindStickyViews(View v) { if (v instanceofViewGroup) { ViewGroup vg =(ViewGroup) v; for (int i = 0; i < vg.getChildCount(); i++) { String tag =getStringTagForView(vg.getChildAt(i)); if (tag != null &&tag.contains(STICKY_TAG)) { stickyViews.add(vg.getChildAt(i)); } else if (vg.getChildAt(i) instanceofViewGroup) { findStickyViews(vg.getChildAt(i)); } } } else{ String tag =(String) v.getTag(); if (tag != null &&tag.contains(STICKY_TAG)) { stickyViews.add(v); } } } privateString getStringTagForView(View v) { Object tagObject =v.getTag(); returnString.valueOf(tagObject); } private voidhideView(View v) { if (Build.VERSION.SDK_INT >= 11) { v.setAlpha(0); } else{ AlphaAnimation anim = new AlphaAnimation(1, 0); anim.setDuration(0); anim.setFillAfter(true); v.startAnimation(anim); } } private voidshowView(View v) { if (Build.VERSION.SDK_INT >= 11) { v.setAlpha(1); } else{ AlphaAnimation anim = new AlphaAnimation(0, 1); anim.setDuration(0); anim.setFillAfter(true); v.startAnimation(anim); } } /*** 设置悬浮高度 * @paramheight */ public void setStickTop(intheight) { MIN_STICK_TOP =height; } /*** 解决vviewpager在scrollview滑动冲突的问题 */ //滑动距离及坐标 private floatxDistance, yDistance, xLast, yLast; @Override public booleanonInterceptTouchEvent(MotionEvent ev) { switch(ev.getAction()) { caseMotionEvent.ACTION_DOWN: xDistance = yDistance =0f; xLast =ev.getX(); yLast =ev.getY(); break; caseMotionEvent.ACTION_MOVE: final float curX =ev.getX(); final float curY =ev.getY(); xDistance += Math.abs(curX -xLast); yDistance += Math.abs(curY -yLast); //com.ihaveu.utils.Log.i("test", "curx:"+curX+",cury:"+curY+",xlast:"+xLast+",ylast:"+yLast); //xLast = curX; //yLast = curY; if (xDistance >yDistance) { return false; } } return super.onInterceptTouchEvent(ev); } }
接下来是调用自定义控件了,用到两个关键的方法。StatusBarUtil.setTranslucentForImageView(MainActivity.this, 0, title)和llTitle.setBackgroundColor(Color.argb((int) alpha, 227, 29, 26))分别设置状态栏和标题栏的颜色。
packagecom.xiaoyuan; importandroid.graphics.Color; importandroid.os.Bundle; importandroid.support.v7.app.AppCompatActivity; importandroid.view.View; importandroid.view.ViewTreeObserver; importandroid.widget.FrameLayout; importandroid.widget.LinearLayout; importandroid.widget.RelativeLayout; importandroid.widget.TextView; importcom.jaeger.library.StatusBarUtil; public class MainActivity extends AppCompatActivity implementsView.OnClickListener, StickyScrollView.OnScrollChangedListener { TextView oneTextView, twoTextView; privateStickyScrollView stickyScrollView; private intheight; privateLinearLayout llContent; privateRelativeLayout llTitle; privateFrameLayout frameLayout; privateTextView title; @Override protected voidonCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); initListeners(); } /*** 初始化View */ private voidinitView() { stickyScrollView =(StickyScrollView) findViewById(R.id.scrollView); frameLayout =(FrameLayout) findViewById(R.id.tabMainContainer); title =(TextView) findViewById(R.id.title); oneTextView =(TextView) findViewById(R.id.infoText); llContent =(LinearLayout) findViewById(R.id.ll_content); llTitle =(RelativeLayout) findViewById(R.id.ll_good_detail); oneTextView.setOnClickListener(this); twoTextView =(TextView) findViewById(R.id.secondText); twoTextView.setOnClickListener(this); stickyScrollView.setOnScrollListener(this); StatusBarUtil.setTranslucentForImageView(MainActivity.this, 0, title); FrameLayout.LayoutParams params =(FrameLayout.LayoutParams) llTitle.getLayoutParams(); params.setMargins(0, getStatusHeight(), 0, 0); llTitle.setLayoutParams(params); //默认设置一个Frg getSupportFragmentManager().beginTransaction().replace(R.id.tabMainContainer, Fragment.newInstance()).commit(); } /*** 获取状态栏高度 * * @return */ private intgetStatusHeight() { int resourceId = MainActivity.this.getResources().getIdentifier("status_bar_height", "dimen", "android"); returngetResources().getDimensionPixelSize(resourceId); } @Override public voidonClick(View v) { if (v.getId() ==R.id.infoText) { getSupportFragmentManager().beginTransaction().replace(R.id.tabMainContainer, Fragment.newInstance()).commit(); } else if (v.getId() ==R.id.secondText) { getSupportFragmentManager().beginTransaction().replace(R.id.tabMainContainer, Fragment1.newInstance()).commit(); } } private voidinitListeners() { //获取内容总高度 final ViewTreeObserver vto =llContent.getViewTreeObserver(); vto.addOnGlobalLayoutListener(newViewTreeObserver.OnGlobalLayoutListener() { @Override public voidonGlobalLayout() { height =llContent.getHeight(); //注意要移除 llContent.getViewTreeObserver() .removeGlobalOnLayoutListener(this); } }); //获取Fragment高度 ViewTreeObserver viewTreeObserver =frameLayout.getViewTreeObserver(); viewTreeObserver.addOnGlobalLayoutListener(newViewTreeObserver.OnGlobalLayoutListener() { @Override public voidonGlobalLayout() { height = height -frameLayout.getHeight(); //注意要移除 frameLayout.getViewTreeObserver() .removeGlobalOnLayoutListener(this); } }); //获取title高度 ViewTreeObserver viewTreeObserver1 =llTitle.getViewTreeObserver(); viewTreeObserver1.addOnGlobalLayoutListener(newViewTreeObserver.OnGlobalLayoutListener() { @Override public voidonGlobalLayout() { height = height - llTitle.getHeight() - getStatusHeight();//计算滑动的总距离 stickyScrollView.setStickTop(llTitle.getHeight() + getStatusHeight());//设置距离多少悬浮 //注意要移除 llTitle.getViewTreeObserver() .removeGlobalOnLayoutListener(this); } }); } @Override public void onScrollChanged(int l, int t, int oldl, intoldt) { if (t <= 0) { llTitle.setBackgroundColor(Color.argb((int) 0, 255, 255, 255)); } else if (t > 0 && t <=height) { float scale = (float) t /height; int alpha = (int) (255 *scale); llTitle.setBackgroundColor(Color.argb((int) alpha, 227, 29, 26));//设置标题栏的透明度及颜色 StatusBarUtil.setTranslucentForImageView(MainActivity.this, alpha, title);//设置状态栏的透明度 } else { StatusBarUtil.setTranslucentForImageView(MainActivity.this, 0, title); llTitle.setBackgroundColor(Color.argb((int) 255, 227, 29, 26)); StatusBarUtil.setTranslucentForImageView(MainActivity.this, 255, title); } } }
最后,尊重一下上述代码的原作者,具体代码可到github下载,https://github.com/xiaoyuanandroid/ProductPage。