无意间了解到沉浸式状态栏,感觉贼拉的高大上,于是就是试着去了解一下,就有了这篇文章。下面就来了解一下啥叫沉浸式状态栏。传统的手机状态栏是呈现出黑色条状的,有的和手机主界面有很明显的区别。这一样就在一定程度上牺牲了视觉宽度,界面面积变小。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 void initState() { 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'
在自定义控件中实现的进本逻辑,代码较长。
package com.xiaoyuan; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.AlphaAnimation; import android.widget.ScrollView; import java.util.ArrayList; /** * @author Emil Sj�lander - [email protected] */ public class StickyScrollView extends ScrollView { /** * 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 ArrayListstickyViews; private View currentlyStickingView; private float stickyViewTopOffset; private int stickyViewLeftOffset; private boolean redirectTouchesToStickyView; private boolean clippingToPadding; private boolean clipToPaddingHasBeenSet; private int mShadowHeight; private Drawable mShadowDrawable; private OnScrollChangedListener mOnScrollHandler = null; private IOnScrollToEnd mOnScrollToEnd = null; private final Runnable invalidateRunnable = new Runnable() { @Override public void run() { 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); } }; public StickyScrollView(Context context) { this(context, null); } public StickyScrollView(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.scrollViewStyle); } public StickyScrollView(Context context, AttributeSet attrs, int defStyle) { 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. * * @param height */ public void setShadowHeight(int height) { mShadowHeight = height; } public void setup() { stickyViews = new ArrayList (); } private int getLeftForViewRelativeOnlyChild(View v) { int left = v.getLeft(); while (v.getParent() != getChildAt(0)) { v = (View) v.getParent(); left += v.getLeft(); } return left; } private int getTopForViewRelativeOnlyChild(View v) { int top = v.getTop(); while (v.getParent() != getChildAt(0)) { v = (View) v.getParent(); top += v.getTop(); } return top; } private int getRightForViewRelativeOnlyChild(View v) { int right = v.getRight(); while (v.getParent() != getChildAt(0)) { v = (View) v.getParent(); right += v.getRight(); } return right; } private int getBottomForViewRelativeOnlyChild(View v) { int bottom = v.getBottom(); while (v.getParent() != getChildAt(0)) { v = (View) v.getParent(); bottom += v.getBottom(); } return bottom; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (!clipToPaddingHasBeenSet) { clippingToPadding = true; } notifyHierarchyChanged(); } @Override public void setClipToPadding(boolean clipToPadding) { super.setClipToPadding(clipToPadding); clippingToPadding = clipToPadding; clipToPaddingHasBeenSet = true; } @Override public void addView(View child) { super.addView(child); findStickyViews(child); } @Override public void addView(View child, int index) { super.addView(child, index); findStickyViews(child); } @Override public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) { super.addView(child, index, params); findStickyViews(child); } @Override public void addView(View child, int width, int height) { super.addView(child, width, height); findStickyViews(child); } @Override public void addView(View child, android.view.ViewGroup.LayoutParams params) { super.addView(child, params); findStickyViews(child); } @Override protected void dispatchDraw(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 boolean dispatchTouchEvent(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 boolean onTouchEvent(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, int oldt) { 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 void setOnScrollListener(OnScrollChangedListener handler) { mOnScrollHandler = handler; } public interface OnScrollChangedListener { public void onScrollChanged(int l, int t, int oldl, int oldt); } public interface IOnScrollToEnd { public void onScrollToEnd(); } public void setOnScrollToEndListener(IOnScrollToEnd handler) { mOnScrollToEnd = handler; } private void doTheStickyThing() { 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 void startStickingView(View viewThatShouldStick) { currentlyStickingView = viewThatShouldStick; if (getStringTagForView(currentlyStickingView).contains(FLAG_HASTRANSPARANCY)) { hideView(currentlyStickingView); } if (((String) currentlyStickingView.getTag()).contains(FLAG_NONCONSTANT)) { post(invalidateRunnable); } } private void stopStickingCurrentlyStickingView() { 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 void notifyStickyAttributeChanged() { notifyHierarchyChanged(); } private void notifyHierarchyChanged() { if (currentlyStickingView != null) { stopStickingCurrentlyStickingView(); } stickyViews.clear(); findStickyViews(getChildAt(0)); doTheStickyThing(); invalidate(); } private void findStickyViews(View v) { if (v instanceof ViewGroup) { 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) instanceof ViewGroup) { findStickyViews(vg.getChildAt(i)); } } } else { String tag = (String) v.getTag(); if (tag != null && tag.contains(STICKY_TAG)) { stickyViews.add(v); } } } private String getStringTagForView(View v) { Object tagObject = v.getTag(); return String.valueOf(tagObject); } private void hideView(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 void showView(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); } } /** * 设置悬浮高度 * @param height */ public void setStickTop(int height) { MIN_STICK_TOP = height; } /** * 解决vviewpager在scrollview滑动冲突的问题 */ // 滑动距离及坐标 private float xDistance, yDistance, xLast, yLast; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: xDistance = yDistance = 0f; xLast = ev.getX(); yLast = ev.getY(); break; case MotionEvent.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))分别设置状态栏和标题栏的颜色。
package com.xiaoyuan; import android.graphics.Color; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import com.jaeger.library.StatusBarUtil; public class MainActivity extends AppCompatActivity implements View.OnClickListener, StickyScrollView.OnScrollChangedListener { TextView oneTextView, twoTextView; private StickyScrollView stickyScrollView; private int height; private LinearLayout llContent; private RelativeLayout llTitle; private FrameLayout frameLayout; private TextView title; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); initListeners(); } /** * 初始化View */ private void initView() { 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 int getStatusHeight() { int resourceId = MainActivity.this.getResources().getIdentifier("status_bar_height", "dimen", "android"); return getResources().getDimensionPixelSize(resourceId); } @Override public void onClick(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 void initListeners() { //获取内容总高度 final ViewTreeObserver vto = llContent.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { height = llContent.getHeight(); //注意要移除 llContent.getViewTreeObserver() .removeGlobalOnLayoutListener(this); } }); //获取Fragment高度 ViewTreeObserver viewTreeObserver = frameLayout.getViewTreeObserver(); viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { height = height - frameLayout.getHeight(); //注意要移除 frameLayout.getViewTreeObserver() .removeGlobalOnLayoutListener(this); } }); //获取title高度 ViewTreeObserver viewTreeObserver1 = llTitle.getViewTreeObserver(); viewTreeObserver1.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { height = height - llTitle.getHeight() - getStatusHeight();//计算滑动的总距离 stickyScrollView.setStickTop(llTitle.getHeight() + getStatusHeight());//设置距离多少悬浮 //注意要移除 llTitle.getViewTreeObserver() .removeGlobalOnLayoutListener(this); } }); } @Override public void onScrollChanged(int l, int t, int oldl, int oldt) { 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。