谷歌並沒有給出沉浸式狀態欄這個概念,谷歌只說了沉浸式模式(Immersive Mode)。 ...
無意間瞭解到沉浸式狀態欄,感覺賊拉的高大上,於是就是試著去瞭解一下,就有了這篇文章。下麵就來瞭解一下啥叫沉浸式狀態欄。傳統的手機狀態欄是呈現出黑色條狀的,有的和手機主界面有很明顯的區別。這一樣就在一定程度上犧牲了視覺寬度,界面面積變小。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 ArrayList<View> stickyViews; 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<View>(); } 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。