想要实现浏览器的多标签切换功能, 需要一个用户交互界面, 这个界面在Android Browser中就是NavScreen了:
这里我们介绍一下下面这个UI的实现, 主要代码在NavScreen.java中.
我们知道, 在Android Browser中 用以和用户打交道的功能基本都被限制在了BaseUI中, 在手机上它的实现就是PhoneUI:
显示多窗口列表当然也是不例外的:PhoneUI::showNavScreen:
02 | void showNavScreen() { |
03 | mUiController.setBlockEvents( true ); |
04 | if (mNavScreen == null ) { |
05 | mNavScreen = new NavScreen(mActivity, mUiController, this ); |
06 | mCustomViewContainer.addView(mNavScreen, COVER_SCREEN_PARAMS); |
08 | mNavScreen.setVisibility(View.VISIBLE); |
09 | mNavScreen.setAlpha(1f); |
10 | mNavScreen.refreshAdapter(); |
13 | if (mAnimScreen == null ) { |
16 | mAnimScreen = new AnimScreen(mActivity); |
18 | mAnimScreen.mMain.setAlpha(1f); |
19 | mAnimScreen.mTitle.setAlpha(1f); |
20 | mAnimScreen.setScaleFactor(1f); |
23 | mAnimScreen.set(getTitleBar(), getWebView()); |
24 | if (mAnimScreen.mMain.getParent() == null ) { |
26 | mCustomViewContainer.addView(mAnimScreen.mMain, COVER_SCREEN_PARAMS); |
28 | mCustomViewContainer.setVisibility(View.VISIBLE); |
29 | mCustomViewContainer.bringToFront(); |
30 | mAnimScreen.mMain.layout( 0 , 0 , mContentView.getWidth(), |
31 | mContentView.getHeight()); |
33 | int fromTop = getTitleBar().getHeight(); |
34 | int fromRight = mContentView.getWidth(); |
35 | int fromBottom = mContentView.getHeight(); |
36 | int width = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_width); |
37 | int height = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_height); |
38 | int ntth = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_titleheight); |
39 | int toLeft = (mContentView.getWidth() - width) / 2 ; |
40 | int toTop = ((fromBottom - (ntth + height)) / 2 + ntth); |
41 | int toRight = toLeft + width; |
42 | int toBottom = toTop + height; |
43 | float scaleFactor = width / ( float ) mContentView.getWidth(); |
44 | detachTab(mActiveTab); |
45 | mContentView.setVisibility(View.GONE); |
46 | AnimatorSet set1 = new AnimatorSet(); |
47 | AnimatorSet inanim = new AnimatorSet(); |
49 | ObjectAnimator tx = ObjectAnimator.ofInt(mAnimScreen.mContent, "left" , |
51 | ObjectAnimator ty = ObjectAnimator.ofInt(mAnimScreen.mContent, "top" , |
53 | ObjectAnimator tr = ObjectAnimator.ofInt(mAnimScreen.mContent, "right" , |
55 | ObjectAnimator tb = ObjectAnimator.ofInt(mAnimScreen.mContent, "bottom" , |
56 | fromBottom, toBottom); |
57 | ObjectAnimator title = ObjectAnimator.ofFloat(mAnimScreen.mTitle, "alpha" , |
59 | ObjectAnimator sx = ObjectAnimator.ofFloat(mAnimScreen, "scaleFactor" , |
62 | ObjectAnimator blend1 = ObjectAnimator.ofFloat(mAnimScreen.mMain, |
64 | blend1.setDuration( 100 ); |
66 | inanim.playTogether(tx, ty, tr, tb, sx, title); |
67 | inanim.setDuration( 200 ); |
68 | set1.addListener( new AnimatorListenerAdapter() { |
70 | public void onAnimationEnd(Animator anim) { |
71 | mCustomViewContainer.removeView(mAnimScreen.mMain); |
73 | mUiController.setBlockEvents( false ); |
76 | set1.playSequentially(inanim, blend1); |
这里还实现了一个打开多窗口的动画, 我们暂时先不去考虑, 先看NavScreen的数据结构:
他的结构也不是很复杂, 拿到了Activity 和Controller的引用, 然后有一个NavTabScroller (继承自NavTabScroller )和 一个TabAdapter (继承自 BaseAdapter)的成员, 他们是多窗口列表view的具体实现和数据来源了. NavScreen有一些Tab的操作, 他们基本都需要通知到Controller, 因为NavScreen只不过是UI 真正的操作是Controller来做的.
看一下NavTabScroller 是一个ScrollView, 多窗口之所以可以滑动就全靠他了, 他还实现了横向竖向滑动, 载入adapter的数据等功能:
01 | public class NavTabScroller extends ScrollerView { |
02 | static final int INVALID_POSITION = - 1 ; |
03 | static final float [] PULL_FACTOR = { 2 .5f, 0 .9f }; |
05 | interface OnRemoveListener { |
06 | public void onRemovePosition( int position); |
09 | interface OnLayoutListener { |
10 | public void onLayout( int l, int t, int r, int b); |
13 | private ContentLayout mContentView; |
14 | private BaseAdapter mAdapter; |
15 | private OnRemoveListener mRemoveListener; |
16 | private OnLayoutListener mLayoutListener; |
18 | private int mGapPosition; |
19 | private ObjectAnimator mGapAnimator; |
22 | private static final float MIN_VELOCITY = 1500 ; |
23 | private AnimatorSet mAnimator; |
25 | private float mFlingVelocity; |
26 | private boolean mNeedsScroll; |
27 | private int mScrollPosition; |
29 | DecelerateInterpolator mCubic; |
他装载数据的操作是setAdapter函数调用handleDataChanged函数实现的:
02 | void handleDataChanged( int newscroll) { |
03 | int scroll = getScrollValue(); |
04 | if (mGapAnimator != null ) { |
05 | mGapAnimator.cancel(); |
07 | mContentView.removeAllViews(); |
08 | for ( int i = 0 ; i < mAdapter.getCount(); i++) { |
09 | View v = mAdapter.getView(i, null , mContentView); |
10 | LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( |
11 | LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); |
12 | lp.gravity = (mHorizontal ? Gravity.CENTER_VERTICAL : Gravity.CENTER_HORIZONTAL); |
13 | mContentView.addView(v, lp); |
14 | if (mGapPosition > INVALID_POSITION){ |
18 | if (newscroll > INVALID_POSITION) { |
19 | newscroll = Math.min(mAdapter.getCount() - 1 , newscroll); |
21 | mScrollPosition = newscroll; |
24 | setScrollValue(scroll); |
好 大体的UI就差不多这些了, 下面是其动画的实现:
其动画分为以下几个:
1.点击多窗口按钮
的时候, 整个浏览器窗口会缩小到多窗口列表, 然后显示出其他的窗口标签供作者选择
2.点击多窗口列表任何一个窗口
其他的多窗口标签会消失 ,
整个窗口会扩到到整个屏幕
3.在多窗口列表中左右滑动任何一个窗口, 整个窗口会渐变和移动 直到删除
4.其实这个"listview"还有回弹功能, 效果是使小窗口的间距缩小,不过效果不是很明显, 应该有点小bug
那就从第一个动画开始分析:
这个动画是在PhoneUI::showNavScreen()函数实现的, 其实就是一个animator动画:
02 | void showNavScreen() { |
03 | mUiController.setBlockEvents( true ); |
04 | if (mNavScreen == null ) { |
05 | mNavScreen = new NavScreen(mActivity, mUiController, this ); |
06 | mCustomViewContainer.addView(mNavScreen, COVER_SCREEN_PARAMS); |
08 | mNavScreen.setVisibility(View.VISIBLE); |
09 | mNavScreen.setAlpha(1f); |
10 | mNavScreen.refreshAdapter(); |
13 | if (mAnimScreen == null ) { |
16 | mAnimScreen = new AnimScreen(mActivity); |
18 | mAnimScreen.mMain.setAlpha(1f); |
19 | mAnimScreen.mTitle.setAlpha(1f); |
20 | mAnimScreen.setScaleFactor(1f); |
23 | mAnimScreen.set(getTitleBar(), getWebView()); |
24 | if (mAnimScreen.mMain.getParent() == null ) { |
26 | mCustomViewContainer.addView(mAnimScreen.mMain, COVER_SCREEN_PARAMS); |
28 | mCustomViewContainer.setVisibility(View.VISIBLE); |
29 | mCustomViewContainer.bringToFront(); |
30 | mAnimScreen.mMain.layout( 0 , 0 , mContentView.getWidth(), |
31 | mContentView.getHeight()); |
33 | int fromTop = getTitleBar().getHeight(); |
34 | int fromRight = mContentView.getWidth(); |
35 | int fromBottom = mContentView.getHeight(); |
36 | int width = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_width); |
37 | int height = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_height); |
38 | int ntth = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_titleheight); |
39 | int toLeft = (mContentView.getWidth() - width) / 2 ; |
40 | int toTop = ((fromBottom - (ntth + height)) / 2 + ntth); |
41 | int toRight = toLeft + width; |
42 | int toBottom = toTop + height; |
43 | float scaleFactor = width / ( float ) mContentView.getWidth(); |
44 | detachTab(mActiveTab); |
45 | mContentView.setVisibility(View.GONE); |
46 | AnimatorSet set1 = new AnimatorSet(); |
47 | AnimatorSet inanim = new AnimatorSet(); |
49 | ObjectAnimator tx = ObjectAnimator.ofInt(mAnimScreen.mContent, "left" , |
51 | ObjectAnimator ty = ObjectAnimator.ofInt(mAnimScreen.mContent, "top" , |
53 | ObjectAnimator tr = ObjectAnimator.ofInt(mAnimScreen.mContent, "right" , |
55 | ObjectAnimator tb = ObjectAnimator.ofInt(mAnimScreen.mContent, "bottom" , |
56 | fromBottom, toBottom); |
57 | ObjectAnimator title = ObjectAnimator.ofFloat(mAnimScreen.mTitle, "alpha" , |
59 | ObjectAnimator sx = ObjectAnimator.ofFloat(mAnimScreen, "scaleFactor" , |
62 | ObjectAnimator blend1 = ObjectAnimator.ofFloat(mAnimScreen.mMain, |
64 | blend1.setDuration( 100 ); |
66 | inanim.playTogether(tx, ty, tr, tb, sx, title); |
67 | inanim.setDuration( 200 ); |
68 | set1.addListener( new AnimatorListenerAdapter() { |
70 | public void onAnimationEnd(Animator anim) { |
71 | mCustomViewContainer.removeView(mAnimScreen.mMain); |
73 | mUiController.setBlockEvents( false ); |
76 | set1.playSequentially(inanim, blend1); |
这里我们可能要问了AnimScreen这个东西是什么呢?原来,为了提高动画的效率,其实是通过把webview的内容绘制到 Imageview 上, 需要切换的时候就把这个imageview添加到webview上面,然后做动画. 看看AnimScreen代码就明白了:
80 | public void set(Bitmap image) { |
81 | mTitle.setVisibility(View.GONE); |
82 | mContent.setImageBitmap(image); |
85 | private void setScaleFactor( float sf) { |
87 | Matrix m = new Matrix(); |
89 | mContent.setImageMatrix(m); |
92 | private float getScaleFactor() { |
知道了第一个动画如何实现, 第二个动画就好理解了, 正好是第一个动画的反过来, 不过这次动画的轨迹可能不一样, 因为用户可能点击的是上面或者最底下的tab:当然, 通过navScreen就可以拿到选择tab的位置:整个操作调用的地方还是比较多的比如选择tab 新建tab等都会调用到这个和动画.
02 | void hideNavScreen( int position, boolean animate) { |
03 | if (!showingNavScreen()) return ; |
04 | final Tab tab = mUiController.getTabControl().getTab(position); |
05 | if ((tab == null ) || !animate) { |
08 | } else if (mTabControl.getTabCount() > 0 ) { |
10 | setActiveTab(mTabControl.getCurrentTab()); |
12 | mContentView.setVisibility(View.VISIBLE); |
16 | NavTabView tabview = (NavTabView) mNavScreen.getTabView(position); |
17 | if (tabview == null ) { |
18 | if (mTabControl.getTabCount() > 0 ) { |
20 | setActiveTab(mTabControl.getCurrentTab()); |
22 | mContentView.setVisibility(View.VISIBLE); |
26 | mUiController.setBlockEvents( true ); |
27 | mUiController.setActiveTab(tab); |
28 | mContentView.setVisibility(View.VISIBLE); |
29 | if (mAnimScreen == null ) { |
30 | mAnimScreen = new AnimScreen(mActivity); |
32 | mAnimScreen.set(tab.getScreenshot()); |
33 | mCustomViewContainer.addView(mAnimScreen.mMain, COVER_SCREEN_PARAMS); |
34 | mAnimScreen.mMain.layout( 0 , 0 , mContentView.getWidth(), |
35 | mContentView.getHeight()); |
36 | mNavScreen.mScroller.finishScroller(); |
37 | ImageView target = tabview.mImage; |
39 | int toTop = getTitleBar().getHeight(); |
40 | int toRight = mContentView.getWidth(); |
41 | int width = target.getDrawable().getIntrinsicWidth(); |
42 | int height = target.getDrawable().getIntrinsicHeight(); |
43 | int fromLeft = tabview.getLeft() + target.getLeft() - mNavScreen.mScroller.getScrollX(); |
44 | int fromTop = tabview.getTop() + target.getTop() - mNavScreen.mScroller.getScrollY(); |
45 | int fromRight = fromLeft + width; |
46 | int fromBottom = fromTop + height; |
47 | float scaleFactor = mContentView.getWidth() / ( float ) width; |
48 | int toBottom = toTop + ( int ) (height * scaleFactor); |
49 | mAnimScreen.mContent.setLeft(fromLeft); |
50 | mAnimScreen.mContent.setTop(fromTop); |
51 | mAnimScreen.mContent.setRight(fromRight); |
52 | mAnimScreen.mContent.setBottom(fromBottom); |
53 | mAnimScreen.setScaleFactor(1f); |
54 | AnimatorSet set1 = new AnimatorSet(); |
55 | ObjectAnimator fade2 = ObjectAnimator.ofFloat(mAnimScreen.mMain, "alpha" , 0f, 1f); |
56 | ObjectAnimator fade1 = ObjectAnimator.ofFloat(mNavScreen, "alpha" , 1f, 0f); |
57 | set1.playTogether(fade1, fade2); |
58 | set1.setDuration( 100 ); |
60 | AnimatorSet set2 = new AnimatorSet(); |
61 | ObjectAnimator l = ObjectAnimator.ofInt(mAnimScreen.mContent, "left" , |
63 | ObjectAnimator t = ObjectAnimator.ofInt(mAnimScreen.mContent, "top" , |
65 | ObjectAnimator r = ObjectAnimator.ofInt(mAnimScreen.mContent, "right" , |
67 | ObjectAnimator b = ObjectAnimator.ofInt(mAnimScreen.mContent, "bottom" , |
68 | fromBottom, toBottom); |
69 | ObjectAnimator scale = ObjectAnimator.ofFloat(mAnimScreen, "scaleFactor" , |
71 | ObjectAnimator otheralpha = ObjectAnimator.ofFloat(mCustomViewContainer, "alpha" , 1f, 0f); |
72 | otheralpha.setDuration( 100 ); |
73 | set2.playTogether(l, t, r, b, scale); |
74 | set2.setDuration( 200 ); |
75 | AnimatorSet combo = new AnimatorSet(); |
76 | combo.playSequentially(set1, set2, otheralpha); |
77 | combo.addListener( new AnimatorListenerAdapter() { |
79 | public void onAnimationEnd(Animator anim) { |
80 | mCustomViewContainer.removeView(mAnimScreen.mMain); |
82 | mUiController.setBlockEvents( false ); |
对于第三个动画, 左右滑动删除的动画, 其入口有二
a. Scrollview的onTouchEvent事件中调用的NavTabScroller::onOrthoDragFinished()函数, 其实最后还是调用到animateOut函数
03 | protected void onOrthoDragFinished(View downView) { |
04 | if (mAnimator != null ) return ; |
05 | if (mIsOrthoDragged && downView != null ) { |
07 | float diff = mHorizontal ? downView.getTranslationY() : downView.getTranslationX(); |
08 | if (Math.abs(diff) > (mHorizontal ? downView.getHeight() : downView.getWidth()) / 2 ) { |
10 | animateOut(downView, Math.signum(diff) * mFlingVelocity, diff); |
13 | offsetView(downView, 0 ); |
在用户按住小tab移动的时候会执行offsetView函数:
1 | private void offsetView(View v, float distance) { |
2 | v.setAlpha(getAlpha(v, distance)); |
5 | v.setTranslationY(distance); |
7 | v.setTranslationX(distance); |
b另一种调用动画的方式比较简单了,其实就是直接调用animateOut函数:
看一下这个函数到底做了什么吧:
1.需要删除窗口的平移和alpha渐变
2.删除窗口后,其他窗口的上移,这是比较复杂的一个逻辑 ,大体是通过改变mGap这个参数实现,动画也是使用了animator:
02 | private void animateOut( final View v, float velocity, float start) { |
03 | if ((v == null ) || (mAnimator != null )) return ; |
04 | final int position = mContentView.indexOfChild(v); |
07 | target = mHorizontal ? -getHeight() : -getWidth(); |
09 | target = mHorizontal ? getHeight() : getWidth(); |
11 | int distance = target - (mHorizontal ? v.getTop() : v.getLeft()); |
12 | long duration = ( long ) (Math.abs(distance) * 1000 / Math.abs(velocity)); |
15 | int gap = mHorizontal ? v.getWidth() : v.getHeight(); |
16 | int centerView = getViewCenter(v); |
17 | int centerScreen = getScreenCenter(); |
18 | int newpos = INVALID_POSITION; |
19 | if (centerView < centerScreen - gap / 2 ) { |
21 | scroll = - (centerScreen - centerView - gap); |
22 | translate = (position > 0 ) ? gap : 0 ; |
24 | } else if (centerView > centerScreen + gap / 2 ) { |
26 | scroll = - (centerScreen + gap - centerView); |
27 | if (position < mAdapter.getCount() - 1 ) { |
32 | scroll = - (centerScreen - centerView); |
33 | if (position < mAdapter.getCount() - 1 ) { |
39 | mGapPosition = position; |
40 | final int pos = newpos; |
41 | ObjectAnimator trans = ObjectAnimator.ofFloat(v, |
42 | (mHorizontal ? TRANSLATION_Y : TRANSLATION_X), start, target); |
43 | ObjectAnimator alpha = ObjectAnimator.ofFloat(v, ALPHA, getAlpha(v,start), |
45 | AnimatorSet set1 = new AnimatorSet(); |
46 | set1.playTogether(trans, alpha); |
47 | set1.setDuration(duration); |
48 | mAnimator = new AnimatorSet(); |
49 | ObjectAnimator trans2 = null ; |
50 | ObjectAnimator scroll1 = null ; |
53 | scroll1 = ObjectAnimator.ofInt( this , "scrollX" , getScrollX(), getScrollX() + scroll); |
55 | scroll1 = ObjectAnimator.ofInt( this , "scrollY" , getScrollY(), getScrollY() + scroll); |
59 | trans2 = ObjectAnimator.ofInt( this , "gap" , 0 , translate); |
61 | final int duration2 = 200 ; |
62 | if (scroll1 != null ) { |
64 | AnimatorSet set2 = new AnimatorSet(); |
65 | set2.playTogether(scroll1, trans2); |
66 | set2.setDuration(duration2); |
67 | mAnimator.playSequentially(set1, set2); |
69 | scroll1.setDuration(duration2); |
70 | mAnimator.playSequentially(set1, scroll1); |
74 | trans2.setDuration(duration2); |
75 | mAnimator.playSequentially(set1, trans2); |
78 | mAnimator.addListener( new AnimatorListenerAdapter() { |
79 | public void onAnimationEnd(Animator a) { |
80 | if (mRemoveListener != null ) { |
81 | mRemoveListener.onRemovePosition(position); |
83 | mGapPosition = INVALID_POSITION; |
85 | handleDataChanged(pos); |
至于切换就简单了,是在controller::setActiveTab()函数进行处理.