前面咱們簡單介紹了Tab和TabControl的大致結構,可是若是想要實現瀏覽器的多標籤切換功能, 還須要一個用戶交互界面, 這個界面在Android Browser中就是NavScreen了: java
這裏咱們介紹一下下面這個UI的實現, 主要代碼在NavScreen.java中. web
咱們知道, 在Android Browser中 用以和用戶打交道的功能基本都被限制在了BaseUI中, 在手機上它的實現就是PhoneUI: 瀏覽器
顯示多窗口列表固然也是不例外的:PhoneUI::showNavScreen: 數據結構
//點擊按鈕顯示多窗口列表 void showNavScreen() { mUiController.setBlockEvents(true); //攔截多窗口外的其餘操做 if (mNavScreen == null) { mNavScreen = new NavScreen(mActivity, mUiController, this); mCustomViewContainer.addView(mNavScreen, COVER_SCREEN_PARAMS); } else { mNavScreen.setVisibility(View.VISIBLE); mNavScreen.setAlpha(1f); mNavScreen.refreshAdapter(); } mActiveTab.capture(); if (mAnimScreen == null) { //這是動畫的視圖 ,多標籤窗口切換的動畫師phoneui來實現的而不是 navscreen ,也就是說我點擊一個tab 剩下的看到的實際上是 //真正的web窗口 mAnimScreen = new AnimScreen(mActivity); } else { mAnimScreen.mMain.setAlpha(1f); mAnimScreen.mTitle.setAlpha(1f); mAnimScreen.setScaleFactor(1f); } //設置動畫須要截圖的view mAnimScreen.set(getTitleBar(), getWebView()); if (mAnimScreen.mMain.getParent() == null) { //若是animscreen 的main沒有父親, 說明是執行了 全屏模式 mCustomViewContainer.addView(mAnimScreen.mMain, COVER_SCREEN_PARAMS); } mCustomViewContainer.setVisibility(View.VISIBLE); mCustomViewContainer.bringToFront();//把這個view放到頂層 mAnimScreen.mMain.layout(0, 0, mContentView.getWidth(), mContentView.getHeight()); //動畫的寬度和contentview同樣大 int fromLeft = 0; int fromTop = getTitleBar().getHeight(); int fromRight = mContentView.getWidth(); int fromBottom = mContentView.getHeight(); int width = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_width); int height = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_height); int ntth = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_titleheight); int toLeft = (mContentView.getWidth() - width) / 2; int toTop = ((fromBottom - (ntth + height)) / 2 + ntth); int toRight = toLeft + width; int toBottom = toTop + height; float scaleFactor = width / (float) mContentView.getWidth(); detachTab(mActiveTab); mContentView.setVisibility(View.GONE); AnimatorSet set1 = new AnimatorSet(); AnimatorSet inanim = new AnimatorSet(); //使用上下左右的位置 使得 tab的運動軌跡 從整個屏幕 位置縮小到tab,不管當前tab在哪裏 ObjectAnimator tx = ObjectAnimator.ofInt(mAnimScreen.mContent, "left", fromLeft, toLeft); ObjectAnimator ty = ObjectAnimator.ofInt(mAnimScreen.mContent, "top", fromTop, toTop); ObjectAnimator tr = ObjectAnimator.ofInt(mAnimScreen.mContent, "right", fromRight, toRight); ObjectAnimator tb = ObjectAnimator.ofInt(mAnimScreen.mContent, "bottom", fromBottom, toBottom); ObjectAnimator title = ObjectAnimator.ofFloat(mAnimScreen.mTitle, "alpha", 1f, 0f); ObjectAnimator sx = ObjectAnimator.ofFloat(mAnimScreen, "scaleFactor", 1f, scaleFactor); ObjectAnimator blend1 = ObjectAnimator.ofFloat(mAnimScreen.mMain, "alpha", 1f, 0f); blend1.setDuration(100); inanim.playTogether(tx, ty, tr, tb, sx, title); inanim.setDuration(200); set1.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator anim) { mCustomViewContainer.removeView(mAnimScreen.mMain); finishAnimationIn(); mUiController.setBlockEvents(false); } }); set1.playSequentially(inanim, blend1);//inanim播放ok後播放 blend1 也就是先縮放而後在透明 set1.start(); }
他的結構也不是很複雜, 拿到了Activity 和Controller的引用, 而後有一個NavTabScroller (繼承自NavTabScroller )和 一個TabAdapter (繼承自 BaseAdapter)的成員, 他們是多窗口列表view的具體實現和數據來源了. NavScreen有一些Tab的操做, 他們基本都須要通知到Controller, 由於NavScreen只不過是UI 真正的操做是Controller來作的. ide
看一下NavTabScroller 是一個ScrollView, 多窗口之因此能夠滑動就全靠他了, 他還實現了橫向豎向滑動, 載入adapter的數據等功能: 函數
public class NavTabScroller extends ScrollerView { static final int INVALID_POSITION = -1; static final float[] PULL_FACTOR = { 2.5f, 0.9f }; interface OnRemoveListener { public void onRemovePosition(int position); } interface OnLayoutListener { public void onLayout(int l, int t, int r, int b); } private ContentLayout mContentView; //其實是一個linearlayout private BaseAdapter mAdapter; private OnRemoveListener mRemoveListener; private OnLayoutListener mLayoutListener; private int mGap; private int mGapPosition; private ObjectAnimator mGapAnimator; // after drag animation velocity in pixels/sec private static final float MIN_VELOCITY = 1500; //最小的滑動 private AnimatorSet mAnimator; private float mFlingVelocity; private boolean mNeedsScroll; private int mScrollPosition; DecelerateInterpolator mCubic; int mPullValue;
他裝載數據的操做是setAdapter函數調用handleDataChanged函數實現的: 佈局
//裝載多窗口數據 void handleDataChanged(int newscroll) { int scroll = getScrollValue(); //是x方向scroll 仍是y if (mGapAnimator != null) { mGapAnimator.cancel();//取消動畫 } mContentView.removeAllViews(); for (int i = 0; i < mAdapter.getCount(); i++) { View v = mAdapter.getView(i, null, mContentView);//從adapter中拿到view 添加到linearlayout上listview等其實也是這樣實現的 LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); lp.gravity = (mHorizontal ? Gravity.CENTER_VERTICAL : Gravity.CENTER_HORIZONTAL); mContentView.addView(v, lp);//添加tabview到那個mContentView . 居中顯示 if (mGapPosition > INVALID_POSITION){ adjustViewGap(v, i); } } if (newscroll > INVALID_POSITION) { newscroll = Math.min(mAdapter.getCount() - 1, newscroll);//newscroll 是從0 開始到 adapter.count的 mNeedsScroll = true; mScrollPosition = newscroll; requestLayout(); } else { setScrollValue(scroll); //滑動到頂部/左邊 } }
好 大致的UI就差很少這些了, 下面是其動畫的實現: post
其動畫分爲如下幾個: 動畫
1.點擊多窗口按鈕 的時候, 整個瀏覽器窗口會縮小到多窗口列表, 而後顯示出其餘的窗口標籤供做者選擇 ui
2.點擊多窗口列表任何一個窗口 其餘的多窗口標籤會消失 ,
整個窗口會擴到到整個屏幕
3.在多窗口列表中左右滑動任何一個窗口, 整個窗口會漸變和移動 直到刪除
4.其實這個"listview"還有回彈功能, 效果是使小窗口的間距縮小,不過效果不是很明顯, 應該有點小bug
那就從第一個動畫開始分析:
這個動畫是在PhoneUI::showNavScreen()函數實現的, 其實就是一個animator動畫:
//點擊按鈕顯示多窗口列表 void showNavScreen() { mUiController.setBlockEvents(true); //攔截多窗口外的其餘操做 if (mNavScreen == null) { mNavScreen = new NavScreen(mActivity, mUiController, this); mCustomViewContainer.addView(mNavScreen, COVER_SCREEN_PARAMS); } else { mNavScreen.setVisibility(View.VISIBLE); mNavScreen.setAlpha(1f); mNavScreen.refreshAdapter(); } mActiveTab.capture(); if (mAnimScreen == null) { //這是動畫的視圖 ,多標籤窗口切換的動畫師phoneui來實現的而不是 navscreen ,也就是說我點擊一個tab 剩下的看到的實際上是 //真正的web窗口 mAnimScreen = new AnimScreen(mActivity); } else { mAnimScreen.mMain.setAlpha(1f); mAnimScreen.mTitle.setAlpha(1f); mAnimScreen.setScaleFactor(1f); } //設置動畫須要截圖的view mAnimScreen.set(getTitleBar(), getWebView()); if (mAnimScreen.mMain.getParent() == null) { //若是animscreen 的main沒有父親, 說明是執行了 全屏模式 mCustomViewContainer.addView(mAnimScreen.mMain, COVER_SCREEN_PARAMS); //把須要作動畫的view添加到整個佈局的上層 } mCustomViewContainer.setVisibility(View.VISIBLE); mCustomViewContainer.bringToFront();//把這個view放到頂層 mAnimScreen.mMain.layout(0, 0, mContentView.getWidth(), mContentView.getHeight()); //動畫的寬度和contentview同樣大 int fromLeft = 0; int fromTop = getTitleBar().getHeight(); int fromRight = mContentView.getWidth(); int fromBottom = mContentView.getHeight(); int width = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_width); int height = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_height); int ntth = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_titleheight); int toLeft = (mContentView.getWidth() - width) / 2; int toTop = ((fromBottom - (ntth + height)) / 2 + ntth); int toRight = toLeft + width; int toBottom = toTop + height; float scaleFactor = width / (float) mContentView.getWidth(); detachTab(mActiveTab); mContentView.setVisibility(View.GONE); AnimatorSet set1 = new AnimatorSet(); AnimatorSet inanim = new AnimatorSet(); //使用上下左右的位置 使得 tab的運動軌跡 從整個屏幕 位置縮小到屏幕的中心 ,不管當前tab在哪裏, 不過軌跡是同樣的 ObjectAnimator tx = ObjectAnimator.ofInt(mAnimScreen.mContent, "left", fromLeft, toLeft); ObjectAnimator ty = ObjectAnimator.ofInt(mAnimScreen.mContent, "top", fromTop, toTop); ObjectAnimator tr = ObjectAnimator.ofInt(mAnimScreen.mContent, "right", fromRight, toRight); ObjectAnimator tb = ObjectAnimator.ofInt(mAnimScreen.mContent, "bottom", fromBottom, toBottom); ObjectAnimator title = ObjectAnimator.ofFloat(mAnimScreen.mTitle, "alpha", 1f, 0f); ObjectAnimator sx = ObjectAnimator.ofFloat(mAnimScreen, "scaleFactor", 1f, scaleFactor); ObjectAnimator blend1 = ObjectAnimator.ofFloat(mAnimScreen.mMain, "alpha", 1f, 0f); blend1.setDuration(100); inanim.playTogether(tx, ty, tr, tb, sx, title); inanim.setDuration(200); set1.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator anim) { mCustomViewContainer.removeView(mAnimScreen.mMain);//把作動畫的view刪除 finishAnimationIn(); mUiController.setBlockEvents(false); } }); set1.playSequentially(inanim, blend1);//inanim播放ok後播放 blend1 也就是先縮放而後在透明 set1.start(); }
/* *其實動畫是使用兩個imageview在作, 這兩個imageview 分別繪製了titlebar和webview */ static class AnimScreen { private View mMain; private ImageView mTitle; private ImageView mContent; private float mScale; private Bitmap mTitleBarBitmap; private Bitmap mContentBitmap; public AnimScreen(Context ctx) { mMain = LayoutInflater.from(ctx).inflate(R.layout.anim_screen, null); mTitle = (ImageView) mMain.findViewById(R.id.title); mContent = (ImageView) mMain.findViewById(R.id.content); mContent.setScaleType(ImageView.ScaleType.MATRIX); mContent.setImageMatrix(new Matrix()); mScale = 1.0f; setScaleFactor(getScaleFactor()); } /** * 包titilebar和webview的截圖畫到動畫的view上 * @param tbar * @param web */ public void set(TitleBar tbar, WebView web) { if (tbar == null || web == null) { return; } if (tbar.getWidth() > 0 && tbar.getEmbeddedHeight() > 0) { if (mTitleBarBitmap == null || mTitleBarBitmap.getWidth() != tbar.getWidth() || mTitleBarBitmap.getHeight() != tbar.getEmbeddedHeight()) { mTitleBarBitmap = safeCreateBitmap(tbar.getWidth(), tbar.getEmbeddedHeight()); } if (mTitleBarBitmap != null) { Canvas c = new Canvas(mTitleBarBitmap); tbar.draw(c); c.setBitmap(null); } } else { mTitleBarBitmap = null; } mTitle.setImageBitmap(mTitleBarBitmap); mTitle.setVisibility(View.VISIBLE); int h = web.getHeight() - tbar.getEmbeddedHeight(); if (mContentBitmap == null || mContentBitmap.getWidth() != web.getWidth() || mContentBitmap.getHeight() != h) { mContentBitmap = safeCreateBitmap(web.getWidth(), h); } if (mContentBitmap != null) { Canvas c = new Canvas(mContentBitmap); int tx = web.getScrollX(); int ty = web.getScrollY(); c.translate(-tx, -ty - tbar.getEmbeddedHeight()); web.draw(c); c.setBitmap(null); } mContent.setImageBitmap(mContentBitmap); } private Bitmap safeCreateBitmap(int width, int height) { if (width <= 0 || height <= 0) { Log.w(LOGTAG, "safeCreateBitmap failed! width: " + width + ", height: " + height); return null; } return Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); } /* * 這個版本至顯示content */ public void set(Bitmap image) { mTitle.setVisibility(View.GONE); mContent.setImageBitmap(image); } private void setScaleFactor(float sf) { mScale = sf; Matrix m = new Matrix(); m.postScale(sf,sf); mContent.setImageMatrix(m); } private float getScaleFactor() { return mScale; } }
知道了第一個動畫如何實現, 第二個動畫就好理解了, 正好是第一個動畫的反過來, 不過此次動畫的軌跡可能不同, 由於用戶可能點擊的是上面或者最底下的tab:固然, 經過navScreen就能夠拿到選擇tab的位置:整個操做調用的地方仍是比較多的好比選擇tab 新建tab等都會調用到這個和動畫.
//隱藏多窗口切換 動畫基本同上面顯示多標籤 void hideNavScreen(int position, boolean animate) { if (!showingNavScreen()) return; final Tab tab = mUiController.getTabControl().getTab(position); if ((tab == null) || !animate) {//彷佛還有別的能夠打開tab的方式可是還不是很清楚在哪裏 if (tab != null) { setActiveTab(tab); } else if (mTabControl.getTabCount() > 0) { // use a fallback tab setActiveTab(mTabControl.getCurrentTab()); } mContentView.setVisibility(View.VISIBLE); finishAnimateOut(); return; } NavTabView tabview = (NavTabView) mNavScreen.getTabView(position); if (tabview == null) { if (mTabControl.getTabCount() > 0) { // use a fallback tab setActiveTab(mTabControl.getCurrentTab()); } mContentView.setVisibility(View.VISIBLE); finishAnimateOut(); return; } mUiController.setBlockEvents(true); mUiController.setActiveTab(tab); mContentView.setVisibility(View.VISIBLE); if (mAnimScreen == null) { mAnimScreen = new AnimScreen(mActivity); } mAnimScreen.set(tab.getScreenshot()); mCustomViewContainer.addView(mAnimScreen.mMain, COVER_SCREEN_PARAMS); //全屏模式 mAnimScreen.mMain.layout(0, 0, mContentView.getWidth(), mContentView.getHeight()); mNavScreen.mScroller.finishScroller(); ImageView target = tabview.mImage; int toLeft = 0; int toTop = getTitleBar().getHeight(); int toRight = mContentView.getWidth(); int width = target.getDrawable().getIntrinsicWidth(); int height = target.getDrawable().getIntrinsicHeight(); int fromLeft = tabview.getLeft() + target.getLeft() - mNavScreen.mScroller.getScrollX(); int fromTop = tabview.getTop() + target.getTop() - mNavScreen.mScroller.getScrollY();//target就是選擇的tab tab的頂部位置 爲了給人以 從原來位置擴大到整個屏幕的感受 int fromRight = fromLeft + width; int fromBottom = fromTop + height; float scaleFactor = mContentView.getWidth() / (float) width; int toBottom = toTop + (int) (height * scaleFactor); mAnimScreen.mContent.setLeft(fromLeft); mAnimScreen.mContent.setTop(fromTop); mAnimScreen.mContent.setRight(fromRight); mAnimScreen.mContent.setBottom(fromBottom); mAnimScreen.setScaleFactor(1f); AnimatorSet set1 = new AnimatorSet(); ObjectAnimator fade2 = ObjectAnimator.ofFloat(mAnimScreen.mMain, "alpha", 0f, 1f); ObjectAnimator fade1 = ObjectAnimator.ofFloat(mNavScreen, "alpha", 1f, 0f); set1.playTogether(fade1, fade2); set1.setDuration(100); //使用上下左右的位置 使得 tab的運動軌跡 從原來位置擴展到整個屏幕,不管整個tab在哪裏 AnimatorSet set2 = new AnimatorSet(); ObjectAnimator l = ObjectAnimator.ofInt(mAnimScreen.mContent, "left", fromLeft, toLeft); ObjectAnimator t = ObjectAnimator.ofInt(mAnimScreen.mContent, "top", fromTop, toTop); ObjectAnimator r = ObjectAnimator.ofInt(mAnimScreen.mContent, "right", fromRight, toRight); ObjectAnimator b = ObjectAnimator.ofInt(mAnimScreen.mContent, "bottom", fromBottom, toBottom); ObjectAnimator scale = ObjectAnimator.ofFloat(mAnimScreen, "scaleFactor", 1f, scaleFactor); ObjectAnimator otheralpha = ObjectAnimator.ofFloat(mCustomViewContainer, "alpha", 1f, 0f); otheralpha.setDuration(100); set2.playTogether(l, t, r, b, scale); set2.setDuration(200); AnimatorSet combo = new AnimatorSet(); combo.playSequentially(set1, set2, otheralpha); combo.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator anim) { mCustomViewContainer.removeView(mAnimScreen.mMain);//動畫結束的時候把動畫view 隱藏 finishAnimateOut();//讓當前窗口漸變消失 mUiController.setBlockEvents(false); } }); combo.start(); }
對於第三個動畫, 左右滑動刪除的動畫, 其入口有二
a. Scrollview的onTouchEvent事件中調用的NavTabScroller::onOrthoDragFinished()函數, 其實最後仍是調用到animateOut函數
/*這是scrollview回調的一個函數,做用是 在用戶左右滑動 tab以後 判斷是否刪除這個tab*/ @Override protected void onOrthoDragFinished(View downView) { if (mAnimator != null) return; if (mIsOrthoDragged && downView != null) { // offset float diff = mHorizontal ? downView.getTranslationY() : downView.getTranslationX(); if (Math.abs(diff) > (mHorizontal ? downView.getHeight() : downView.getWidth()) / 2) { // remove it 達到了刪除tab的調節,開始刪除 animateOut(downView, Math.signum(diff) * mFlingVelocity, diff); } else { // snap back 沒有達到條件,就讓view回來 offsetView(downView, 0); } } }
在用戶按住小tab移動的時候會執行offsetView函數:
private void offsetView(View v, float distance) { v.setAlpha(getAlpha(v, distance)); //setTranslationY 這個功能應該只有3.0之後才支持 讓view左右滑動 if (mHorizontal) { v.setTranslationY(distance); } else { v.setTranslationX(distance); } }
b另外一種調用動畫的方式比較簡單了,其實就是直接調用animateOut函數:
看一下這個函數到底作了什麼吧:
1.須要刪除窗口的平移和alpha漸變
2.刪除窗口後,其餘窗口的上移,這是比較複雜的一個邏輯 ,大致是經過改變mGap這個參數實現,動畫也是使用了animator:
/*刪除 tab 動畫 (左右滑動刪除 )的顯示*/ private void animateOut(final View v, float velocity, float start) { if ((v == null) || (mAnimator != null)) return; //有其餘動畫就不要執行這個動畫 final int position = mContentView.indexOfChild(v); int target = 0; if (velocity < 0) {//動畫結束的位置 target = mHorizontal ? -getHeight() : -getWidth(); } else { target = mHorizontal ? getHeight() : getWidth(); } int distance = target - (mHorizontal ? v.getTop() : v.getLeft()); long duration = (long) (Math.abs(distance) * 1000 / Math.abs(velocity));//動畫持續時間 int scroll = 0; int translate = 0; int gap = mHorizontal ? v.getWidth() : v.getHeight(); int centerView = getViewCenter(v);//獲取view的中心 int centerScreen = getScreenCenter();//獲取屏幕的中心 int newpos = INVALID_POSITION; if (centerView < centerScreen - gap / 2) { // top view刪除的是上面的view scroll = - (centerScreen - centerView - gap); translate = (position > 0) ? gap : 0; newpos = position; } else if (centerView > centerScreen + gap / 2) { // bottom view 刪除的是底部的view scroll = - (centerScreen + gap - centerView); if (position < mAdapter.getCount() - 1) { translate = -gap; } } else { // center view 刪除的是中間的view scroll = - (centerScreen - centerView); if (position < mAdapter.getCount() - 1) { translate = -gap; } else { scroll -= gap; } } mGapPosition = position; final int pos = newpos; ObjectAnimator trans = ObjectAnimator.ofFloat(v, (mHorizontal ? TRANSLATION_Y : TRANSLATION_X), start, target); //控制待刪除view的水平 移動 ObjectAnimator alpha = ObjectAnimator.ofFloat(v, ALPHA, getAlpha(v,start),//控制待刪除view的透明變化 getAlpha(v,target)); AnimatorSet set1 = new AnimatorSet(); set1.playTogether(trans, alpha); set1.setDuration(duration); mAnimator = new AnimatorSet(); ObjectAnimator trans2 = null; ObjectAnimator scroll1 = null; if (scroll != 0) { if (mHorizontal) {//調整scrollview的scroll位置 scroll1 = ObjectAnimator.ofInt(this, "scrollX", getScrollX(), getScrollX() + scroll); } else { scroll1 = ObjectAnimator.ofInt(this, "scrollY", getScrollY(), getScrollY() + scroll); } } if (translate != 0) { trans2 = ObjectAnimator.ofInt(this, "gap", 0, translate); //刪除view會留下一個空白,須要讓上面的view補充上 這裏gap是 負值,由於view少了,座標也就小了 } final int duration2 = 200; if (scroll1 != null) { if (trans2 != null) { AnimatorSet set2 = new AnimatorSet(); set2.playTogether(scroll1, trans2); set2.setDuration(duration2); mAnimator.playSequentially(set1, set2); } else { scroll1.setDuration(duration2); mAnimator.playSequentially(set1, scroll1); } } else { if (trans2 != null) { trans2.setDuration(duration2); mAnimator.playSequentially(set1, trans2); } } mAnimator.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator a) { if (mRemoveListener != null) { mRemoveListener.onRemovePosition(position);//通知移除tab mAnimator = null; mGapPosition = INVALID_POSITION; mGap = 0; handleDataChanged(pos); } } }); mAnimator.start(); }
至於切換就簡單了,是在controller::setActiveTab()函數進行處理.