TabBar.java revision 5d43ce897758b7cb5deadc1203717b965334784c
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.browser;
18
19import com.android.browser.BrowserWebView.ScrollListener;
20
21import android.animation.Animator;
22import android.animation.Animator.AnimatorListener;
23import android.animation.AnimatorSet;
24import android.animation.ObjectAnimator;
25import android.app.Activity;
26import android.content.Context;
27import android.content.res.Configuration;
28import android.content.res.Resources;
29import android.graphics.Bitmap;
30import android.graphics.BitmapShader;
31import android.graphics.Canvas;
32import android.graphics.Matrix;
33import android.graphics.Paint;
34import android.graphics.Path;
35import android.graphics.Shader;
36import android.graphics.drawable.BitmapDrawable;
37import android.graphics.drawable.Drawable;
38import android.graphics.drawable.LayerDrawable;
39import android.graphics.drawable.PaintDrawable;
40import android.view.ContextMenu;
41import android.view.Gravity;
42import android.view.LayoutInflater;
43import android.view.MenuInflater;
44import android.view.View;
45import android.view.View.OnClickListener;
46import android.webkit.WebView;
47import android.widget.ImageButton;
48import android.widget.ImageView;
49import android.widget.LinearLayout;
50import android.widget.TextView;
51
52import java.util.HashMap;
53import java.util.List;
54import java.util.Map;
55
56/**
57 * tabbed title bar for xlarge screen browser
58 */
59public class TabBar extends LinearLayout
60        implements ScrollListener, OnClickListener {
61
62    private static final int PROGRESS_MAX = 100;
63
64    private Activity mActivity;
65    private UiController mUiController;
66    private TabControl mTabControl;
67    private XLargeUi mUi;
68
69    private int mTabWidthSelected;
70    private int mTabWidthUnselected;
71
72    private TabScrollView mTabs;
73
74    private ImageButton mNewTab;
75    private int mButtonWidth;
76
77    private Map<Tab, TabView> mTabMap;
78
79    private Drawable mGenericFavicon;
80
81    private int mCurrentTextureWidth = 0;
82    private int mCurrentTextureHeight = 0;
83
84    private Drawable mActiveDrawable;
85    private Drawable mInactiveDrawable;
86
87    private final Paint mActiveShaderPaint = new Paint();
88    private final Paint mInactiveShaderPaint = new Paint();
89    private final Paint mFocusPaint = new Paint();
90    private final Matrix mActiveMatrix = new Matrix();
91    private final Matrix mInactiveMatrix = new Matrix();
92
93    private BitmapShader mActiveShader;
94    private BitmapShader mInactiveShader;
95
96    private int mTabOverlap;
97    private int mAddTabOverlap;
98    private int mTabSliceWidth;
99    private boolean mUseQuickControls;
100
101    public TabBar(Activity activity, UiController controller, XLargeUi ui) {
102        super(activity);
103        mActivity = activity;
104        mUiController = controller;
105        mTabControl = mUiController.getTabControl();
106        mUi = ui;
107        Resources res = activity.getResources();
108        mTabWidthSelected = (int) res.getDimension(R.dimen.tab_width_selected);
109        mTabWidthUnselected = (int) res.getDimension(R.dimen.tab_width_unselected);
110        mActiveDrawable = res.getDrawable(R.drawable.bg_urlbar);
111        mInactiveDrawable = res.getDrawable(R.drawable.browsertab_inactive);
112
113        mTabMap = new HashMap<Tab, TabView>();
114        LayoutInflater factory = LayoutInflater.from(activity);
115        factory.inflate(R.layout.tab_bar, this);
116        setPadding(0, (int) res.getDimension(R.dimen.tab_padding_top), 0, 0);
117        mTabs = (TabScrollView) findViewById(R.id.tabs);
118        mNewTab = (ImageButton) findViewById(R.id.newtab);
119        mNewTab.setOnClickListener(this);
120        mGenericFavicon = res.getDrawable(R.drawable.app_web_browser_sm);
121
122        updateTabs(mUiController.getTabs());
123        mButtonWidth = -1;
124        // tab dimensions
125        mTabOverlap = (int) res.getDimension(R.dimen.tab_overlap);
126        mAddTabOverlap = (int) res.getDimension(R.dimen.tab_addoverlap);
127        mTabSliceWidth = (int) res.getDimension(R.dimen.tab_slice);
128
129        mActiveShaderPaint.setStyle(Paint.Style.FILL);
130        mActiveShaderPaint.setAntiAlias(true);
131
132        mInactiveShaderPaint.setStyle(Paint.Style.FILL);
133        mInactiveShaderPaint.setAntiAlias(true);
134
135        mFocusPaint.setStyle(Paint.Style.STROKE);
136        mFocusPaint.setStrokeWidth(res.getDimension(R.dimen.tab_focus_stroke));
137        mFocusPaint.setAntiAlias(true);
138        mFocusPaint.setColor(res.getColor(R.color.tabFocusHighlight));
139    }
140
141    @Override
142    public void onConfigurationChanged(Configuration config) {
143        super.onConfigurationChanged(config);
144        Resources res = mActivity.getResources();
145        mTabWidthSelected = (int) res.getDimension(R.dimen.tab_width_selected);
146        mTabWidthUnselected = (int) res.getDimension(R.dimen.tab_width_unselected);
147        // force update of tab bar
148        mTabs.updateLayout();
149    }
150
151    void setUseQuickControls(boolean useQuickControls) {
152        mUseQuickControls = useQuickControls;
153        mNewTab.setVisibility(mUseQuickControls ? View.GONE
154                : View.VISIBLE);
155    }
156
157    int getTabCount() {
158        return mTabMap.size();
159    }
160
161    void updateTabs(List<Tab> tabs) {
162        mTabs.clearTabs();
163        mTabMap.clear();
164        for (Tab tab : tabs) {
165            TabView tv = buildTabView(tab);
166            mTabs.addTab(tv);
167        }
168        mTabs.setSelectedTab(mTabControl.getCurrentPosition());
169    }
170
171    @Override
172    protected void onMeasure(int hspec, int vspec) {
173        super.onMeasure(hspec, vspec);
174        int w = getMeasuredWidth();
175        // adjust for new tab overlap
176        if (!mUseQuickControls) {
177            w -= mAddTabOverlap;
178        }
179        setMeasuredDimension(w, getMeasuredHeight());
180    }
181
182    @Override
183    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
184        // use paddingLeft and paddingTop
185        int pl = getPaddingLeft();
186        int pt = getPaddingTop();
187        int sw = mTabs.getMeasuredWidth();
188        int w = right - left - pl;
189        if (mUseQuickControls) {
190            mButtonWidth = 0;
191        } else {
192            mButtonWidth = mNewTab.getMeasuredWidth() - mAddTabOverlap;
193            if (w-sw < mButtonWidth) {
194                sw = w - mButtonWidth;
195            }
196        }
197        mTabs.layout(pl, pt, pl + sw, bottom - top);
198        // adjust for overlap
199        if (!mUseQuickControls) {
200            mNewTab.layout(pl + sw - mAddTabOverlap, pt,
201                    pl + sw + mButtonWidth - mAddTabOverlap, bottom - top);
202        }
203    }
204
205    public void onClick(View view) {
206        if (mNewTab == view) {
207            mUiController.openTabToHomePage();
208        } else if (mTabs.getSelectedTab() == view) {
209            if (mUseQuickControls) {
210                if (mUi.isTitleBarShowing() && !isLoading()) {
211                    mUi.stopEditingUrl();
212                    mUi.hideTitleBar();
213                } else {
214                    mUi.stopWebViewScrolling();
215                    mUi.editUrl(false);
216                }
217            } else if (mUi.isTitleBarShowing() && !isLoading()) {
218                mUi.stopEditingUrl();
219                mUi.hideTitleBar();
220            } else {
221                showUrlBar();
222            }
223        } else if (view instanceof TabView) {
224            final Tab tab = ((TabView) view).mTab;
225            int ix = mTabs.getChildIndex(view);
226            if (ix >= 0) {
227                mTabs.setSelectedTab(ix);
228                mUiController.switchToTab(tab);
229            }
230        }
231    }
232
233    private void showUrlBar() {
234        mUi.stopWebViewScrolling();
235        mUi.showTitleBar();
236    }
237
238    void showTitleBarIndicator(boolean show) {
239        Tab tab = mTabControl.getCurrentTab();
240        if (tab != null && !tab.isSnapshot()) {
241            TabView tv = mTabMap.get(tab);
242            if (tv != null) {
243                tv.showIndicator(show);
244            }
245        }
246    }
247
248    boolean showsTitleBarIndicator() {
249        Tab tab = mTabControl.getCurrentTab();
250        if (tab != null) {
251            TabView tv = mTabMap.get(tab);
252            if (tv != null) {
253                return tv.showsIndicator();
254            }
255        }
256        return false;
257    }
258
259    // callback after fake titlebar is shown
260    void onShowTitleBar() {
261        showTitleBarIndicator(false);
262    }
263
264    // callback after fake titlebar is hidden
265    void onHideTitleBar() {
266        Tab tab = mTabControl.getCurrentTab();
267        WebView w = tab.getWebView();
268        if (w != null) {
269            showTitleBarIndicator(w.getVisibleTitleHeight() == 0);
270        }
271    }
272
273    // webview scroll listener
274
275    @Override
276    public void onScroll(int visibleTitleHeight, boolean userInitiated) {
277        if (mUseQuickControls) return;
278        // isLoading is using the current tab, which initially might not be set yet
279        if (mTabControl.getCurrentTab() != null
280                && !isLoading()) {
281            if (visibleTitleHeight == 0) {
282                if (!showsTitleBarIndicator()
283                        && (!mUi.isEditingUrl() || userInitiated)) {
284                    mUi.hideTitleBar();
285                    showTitleBarIndicator(true);
286                }
287            } else {
288                if (showsTitleBarIndicator()) {
289                    showTitleBarIndicator(false);
290                }
291            }
292        }
293    }
294
295    @Override
296    public void createContextMenu(ContextMenu menu) {
297        MenuInflater inflater = mActivity.getMenuInflater();
298        inflater.inflate(R.menu.title_context, menu);
299        mActivity.onCreateContextMenu(menu, this, null);
300    }
301
302    private TabView buildTabView(Tab tab) {
303        TabView tabview = new TabView(mActivity, tab);
304        mTabMap.put(tab, tabview);
305        tabview.setOnClickListener(this);
306        return tabview;
307    }
308
309    private static Bitmap getDrawableAsBitmap(Drawable drawable, int width, int height) {
310        Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
311        Canvas c = new Canvas(b);
312        drawable.setBounds(0, 0, width, height);
313        drawable.draw(c);
314        return b;
315    }
316
317    /**
318     * View used in the tab bar
319     */
320    class TabView extends LinearLayout implements OnClickListener {
321
322        Tab mTab;
323        View mTabContent;
324        TextView mTitle;
325        View mIndicator;
326        View mIncognito;
327        View mSnapshot;
328        ImageView mIconView;
329        ImageView mLock;
330        ImageView mClose;
331        boolean mSelected;
332        boolean mInLoad;
333        Path mPath;
334        Path mFocusPath;
335        int[] mWindowPos;
336
337        /**
338         * @param context
339         */
340        public TabView(Context context, Tab tab) {
341            super(context);
342            setWillNotDraw(false);
343            mPath = new Path();
344            mFocusPath = new Path();
345            mWindowPos = new int[2];
346            mTab = tab;
347            setGravity(Gravity.CENTER_VERTICAL);
348            setOrientation(LinearLayout.HORIZONTAL);
349            setPadding(mTabOverlap, 0, mTabSliceWidth, 0);
350            LayoutInflater inflater = LayoutInflater.from(getContext());
351            mTabContent = inflater.inflate(R.layout.tab_title, this, true);
352            mTitle = (TextView) mTabContent.findViewById(R.id.title);
353            mIconView = (ImageView) mTabContent.findViewById(R.id.favicon);
354            mLock = (ImageView) mTabContent.findViewById(R.id.lock);
355            mClose = (ImageView) mTabContent.findViewById(R.id.close);
356            mClose.setOnClickListener(this);
357            mIncognito = mTabContent.findViewById(R.id.incognito);
358            mSnapshot = mTabContent.findViewById(R.id.snapshot);
359            mIndicator = mTabContent.findViewById(R.id.chevron);
360            mSelected = false;
361            mInLoad = false;
362            // update the status
363            updateFromTab();
364        }
365
366        void showIndicator(boolean show) {
367            if (mSelected) {
368                mIndicator.setVisibility(show ? View.VISIBLE : View.GONE);
369                LayoutParams lp = (LinearLayout.LayoutParams) getLayoutParams();
370                if (show) {
371                    lp.width = mTabWidthSelected + mIndicator.getWidth();
372                } else {
373                    lp.width = mTabWidthSelected;
374                }
375                lp.height =  LayoutParams.MATCH_PARENT;
376                setLayoutParams(lp);
377            } else {
378                mIndicator.setVisibility(View.GONE);
379            }
380        }
381
382        boolean showsIndicator() {
383            return (mIndicator.getVisibility() == View.VISIBLE);
384        }
385
386        @Override
387        public void onClick(View v) {
388            if (v == mClose) {
389                closeTab();
390            }
391        }
392
393        private void updateFromTab() {
394            String displayTitle = mTab.getTitle();
395            if (displayTitle == null) {
396                displayTitle = mTab.getUrl();
397            }
398            setDisplayTitle(displayTitle);
399            setProgress(mTab.getLoadProgress());
400            if (mTab.getFavicon() != null) {
401                setFavicon(renderFavicon(mTab.getFavicon()));
402            }
403            updateTabIcons();
404        }
405
406        private void updateTabIcons() {
407            mIncognito.setVisibility(
408                    mTab.isPrivateBrowsingEnabled() ?
409                    View.VISIBLE : View.GONE);
410            mSnapshot.setVisibility(mTab.isSnapshot()
411                    ? View.VISIBLE : View.GONE);
412        }
413
414        @Override
415        public void setActivated(boolean selected) {
416            mSelected = selected;
417            mClose.setVisibility(mSelected ? View.VISIBLE : View.GONE);
418            mIndicator.setVisibility(View.GONE);
419            mTitle.setTextAppearance(mActivity, mSelected ?
420                    R.style.TabTitleSelected : R.style.TabTitleUnselected);
421            setHorizontalFadingEdgeEnabled(!mSelected);
422            super.setActivated(selected);
423            updateLayoutParams();
424            setFocusable(!selected);
425            postInvalidate();
426        }
427
428        public void updateLayoutParams() {
429            LayoutParams lp = (LinearLayout.LayoutParams) getLayoutParams();
430            lp.width = mSelected ? mTabWidthSelected : mTabWidthUnselected;
431            lp.height =  LayoutParams.MATCH_PARENT;
432            setLayoutParams(lp);
433        }
434
435        void setDisplayTitle(String title) {
436            mTitle.setText(title);
437        }
438
439        void setFavicon(Drawable d) {
440            mIconView.setImageDrawable(d);
441        }
442
443        void setLock(Drawable d) {
444            if (null == d) {
445                mLock.setVisibility(View.GONE);
446            } else {
447                mLock.setImageDrawable(d);
448                mLock.setVisibility(View.VISIBLE);
449            }
450        }
451
452        void setProgress(int newProgress) {
453            if (newProgress >= PROGRESS_MAX) {
454                mInLoad = false;
455            } else {
456                if (!mInLoad && getWindowToken() != null) {
457                    mInLoad = true;
458                }
459            }
460        }
461
462        private void closeTab() {
463            if (mTab == mTabControl.getCurrentTab()) {
464                mUiController.closeCurrentTab();
465            } else {
466                mUiController.closeTab(mTab);
467            }
468        }
469
470        @Override
471        protected void onLayout(boolean changed, int l, int t, int r, int b) {
472            super.onLayout(changed, l, t, r, b);
473            setTabPath(mPath, 0, 0, r - l, b - t);
474            setFocusPath(mFocusPath, 0, 0, r - l, b - t);
475        }
476
477        @Override
478        protected void dispatchDraw(Canvas canvas) {
479            if (mCurrentTextureWidth != mUi.getContentWidth() ||
480                    mCurrentTextureHeight != getHeight()) {
481                mCurrentTextureWidth = mUi.getContentWidth();
482                mCurrentTextureHeight = getHeight();
483
484                if (mCurrentTextureWidth > 0 && mCurrentTextureHeight > 0) {
485                    Bitmap activeTexture = getDrawableAsBitmap(mActiveDrawable,
486                            mCurrentTextureWidth, mCurrentTextureHeight);
487                    Bitmap inactiveTexture = getDrawableAsBitmap(mInactiveDrawable,
488                            mCurrentTextureWidth, mCurrentTextureHeight);
489
490                    mActiveShader = new BitmapShader(activeTexture,
491                            Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
492                    mActiveShaderPaint.setShader(mActiveShader);
493
494                    mInactiveShader = new BitmapShader(inactiveTexture,
495                            Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
496                    mInactiveShaderPaint.setShader(mInactiveShader);
497                }
498            }
499            // add some monkey protection
500            if ((mActiveShader != null) && (mInactiveShader != null)) {
501                int state = canvas.save();
502                getLocationInWindow(mWindowPos);
503                Paint paint = mSelected ? mActiveShaderPaint : mInactiveShaderPaint;
504                drawClipped(canvas, paint, mPath, mWindowPos[0]);
505                canvas.restoreToCount(state);
506            }
507            super.dispatchDraw(canvas);
508        }
509
510        private void drawClipped(Canvas canvas, Paint paint, Path clipPath, int left) {
511            // TODO: We should change the matrix/shader only when needed
512            final Matrix matrix = mSelected ? mActiveMatrix : mInactiveMatrix;
513            matrix.setTranslate(-left, 0.0f);
514            (mSelected ? mActiveShader : mInactiveShader).setLocalMatrix(matrix);
515            canvas.drawPath(clipPath, paint);
516            if (isFocused()) {
517                canvas.drawPath(mFocusPath, mFocusPaint);
518            }
519        }
520
521        private void setTabPath(Path path, int l, int t, int r, int b) {
522            path.reset();
523            path.moveTo(l, b);
524            path.lineTo(l, t);
525            path.lineTo(r - mTabSliceWidth, t);
526            path.lineTo(r, b);
527            path.close();
528        }
529
530        private void setFocusPath(Path path, int l, int t, int r, int b) {
531            path.reset();
532            path.moveTo(l, b);
533            path.lineTo(l, t);
534            path.lineTo(r - mTabSliceWidth, t);
535            path.lineTo(r, b);
536        }
537
538    }
539
540    static Drawable createFaviconBackground(Context context) {
541        PaintDrawable faviconBackground = new PaintDrawable();
542        Resources res = context.getResources();
543        faviconBackground.getPaint().setColor(context.getResources()
544                .getColor(R.color.tabFaviconBackground));
545        faviconBackground.setCornerRadius(
546                res.getDimension(R.dimen.tab_favicon_corner_radius));
547        return faviconBackground;
548    }
549
550    private Drawable renderFavicon(Bitmap icon) {
551        Drawable[] array = new Drawable[2];
552        array[0] = createFaviconBackground(getContext());
553        if (icon == null) {
554            array[1] = mGenericFavicon;
555        } else {
556            array[1] = new BitmapDrawable(icon);
557        }
558        LayerDrawable d = new LayerDrawable(array);
559        d.setLayerInset(1, 2, 2, 2, 2);
560        return d;
561    }
562
563    private void animateTabOut(final Tab tab, final TabView tv) {
564        ObjectAnimator scalex = ObjectAnimator.ofFloat(tv, "scaleX", 1.0f, 0.0f);
565        ObjectAnimator scaley = ObjectAnimator.ofFloat(tv, "scaleY", 1.0f, 0.0f);
566        ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "alpha", 1.0f, 0.0f);
567        AnimatorSet animator = new AnimatorSet();
568        animator.playTogether(scalex, scaley, alpha);
569        animator.setDuration(150);
570        animator.addListener(new AnimatorListener() {
571
572            @Override
573            public void onAnimationCancel(Animator animation) {
574            }
575
576            @Override
577            public void onAnimationEnd(Animator animation) {
578                mTabs.removeTab(tv);
579                mTabMap.remove(tab);
580                mUi.onRemoveTabCompleted(tab);
581            }
582
583            @Override
584            public void onAnimationRepeat(Animator animation) {
585            }
586
587            @Override
588            public void onAnimationStart(Animator animation) {
589            }
590
591        });
592        animator.start();
593    }
594
595    private void animateTabIn(final Tab tab, final TabView tv) {
596        ObjectAnimator scalex = ObjectAnimator.ofFloat(tv, "scaleX", 0.0f, 1.0f);
597        scalex.setDuration(150);
598        scalex.addListener(new AnimatorListener() {
599
600            @Override
601            public void onAnimationCancel(Animator animation) {
602            }
603
604            @Override
605            public void onAnimationEnd(Animator animation) {
606                mUi.onAddTabCompleted(tab);
607            }
608
609            @Override
610            public void onAnimationRepeat(Animator animation) {
611            }
612
613            @Override
614            public void onAnimationStart(Animator animation) {
615                mTabs.addTab(tv);
616            }
617
618        });
619        scalex.start();
620    }
621
622    // TabChangeListener implementation
623
624    public void onSetActiveTab(Tab tab) {
625        mTabs.setSelectedTab(mTabControl.getTabPosition(tab));
626        TabView tv = mTabMap.get(tab);
627        if (tv != null) {
628            tv.setProgress(tv.mTab.getLoadProgress());
629            // update the scroll state
630            WebView webview = tab.getWebView();
631            if (webview != null) {
632                int h = webview.getVisibleTitleHeight();
633                onScroll(h, true);
634            }
635        }
636    }
637
638    public void onFavicon(Tab tab, Bitmap favicon) {
639        TabView tv = mTabMap.get(tab);
640        if (tv != null) {
641            tv.setFavicon(renderFavicon(favicon));
642        }
643    }
644
645    public void onNewTab(Tab tab) {
646        TabView tv = buildTabView(tab);
647        animateTabIn(tab, tv);
648    }
649
650    public void onProgress(Tab tab, int progress) {
651        TabView tv = mTabMap.get(tab);
652        if (tv != null) {
653            tv.setProgress(progress);
654        }
655    }
656
657    public void onRemoveTab(Tab tab) {
658        TabView tv = mTabMap.get(tab);
659        if (tv != null) {
660            animateTabOut(tab, tv);
661        } else {
662            mTabMap.remove(tab);
663        }
664    }
665
666    public void onUrlAndTitle(Tab tab, String url, String title) {
667        TabView tv = mTabMap.get(tab);
668        if (tv != null) {
669            if (title != null) {
670                tv.setDisplayTitle(title);
671            } else if (url != null) {
672                tv.setDisplayTitle(UrlUtils.stripUrl(url));
673            }
674            tv.updateTabIcons();
675        }
676    }
677
678    private boolean isLoading() {
679        TabView tv = mTabMap.get(mTabControl.getCurrentTab());
680        if (tv != null) {
681            return tv.mInLoad;
682        } else {
683            return false;
684        }
685    }
686
687}
688