版本:1.0 日期:2014.11.10 2014.11.11 版权:© 2014 kince 转载注明出处
一、概述
    桌面抽屉之间的切换时Android用户经常触发的行为,好的交互会给用户一个舒适的体验。百度桌面默认是随机切换不同的动画,Android默认是一个大小和透明的渐变的动画,如下:
     下面开始分析在Launcher2(KitKat)的源码里面是如何实现这种效果的。
二、下面列举相关的方法和变量
4082:interface LauncherTransitionable {    View getContent();    void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace);    void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace);    void onLauncherTransitionStep(Launcher l, float t);    void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace);}2948:exitSpringLoadedDragMode()2926:exitSpringLoadedDragModeDelayed()2918:enterSpringLoadedDragMode()2899:showAllApps()2864:showWorkspace()2860:showWorkspace()2740:hideAppsCustomizeHelper()2573:showAppsCustomizeHelper()2498:dispatchOnLauncherTransitionPrepare()2097:onClickAllAppsButton()2048:onTouch()2008:onClick()1976:onBackPressed()1456:onNewIntent1273:mReceiver749:onResume()

三、分析
  首先从最直观的方式开始,就是Dock栏进入抽屉的按钮。点击它会从桌面到抽屉,进入抽屉后再按返回键会从抽屉到桌面。这个按钮在Launcher类中对应的变量是 mAllAppsButton, 因为Launcher类继承了 View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks,View.OnTouchListener这几个接口,所以点击事情需要由 onClick(View v)方法来处理。在Launcher类的 onClick(View v)方法中,
/**     * Launches the intent referred by the clicked shortcut.     *     * @param v The view representing the clicked shortcut.     */    public void onClick(View v) {        // Make sure that rogue clicks don't get through while allapps is launching, or after the        // view has detached (it's possible for this to happen if the view is removed mid touch).        if (v.getWindowToken() == null) {            return;        }        if (!mWorkspace .isFinishedSwitchingState()) {            return;        }        Object tag = v.getTag();        if (tag instanceof ShortcutInfo) {            // Open shortcut            final Intent intent = ((ShortcutInfo) tag).intent;            int[] pos = new int[2];            v.getLocationOnScreen(pos);            intent.setSourceBounds( new Rect(pos[0], pos[1],                    pos[0] + v.getWidth(), pos[1] + v.getHeight()));            boolean success = startActivitySafely(v, intent, tag);            if (success && v instanceof BubbleTextView) {                mWaitingForResume = (BubbleTextView) v;                mWaitingForResume.setStayPressed(true);            }        } else if (tag instanceof FolderInfo) {            if (v instanceof FolderIcon) {                FolderIcon fi = (FolderIcon) v;                handleFolderClick(fi);            }        } else if (v == mAllAppsButton ) {            if (isAllAppsVisible()) {                showWorkspace( true);            } else {                onClickAllAppsButton(v);            }        }    }
     从标注地方可以看出,首先对View进行一个判断,如果是mAllAppsButton则开始下面的判断。如果是在抽屉里面,则进入到桌面;如果不是抽屉,则调用onClickAllAppsButton(v)方法。而onClickAllAppsButton(v)方法就是调用showAllApps方法,顾名思义就是进入后抽屉显示所有的app。接着在抽屉里面,如果要返回桌面,按Back键的话会调用onKeyDown或者onBackPressed()方法。   
 @Override    public boolean onKeyDown( int keyCode, KeyEvent event) {        final int uniChar = event.getUnicodeChar();        final boolean handled = super.onKeyDown(keyCode, event);        final boolean isKeyNotWhitespace = uniChar > 0 && !Character.isWhitespace(uniChar);        if (!handled && acceptFilter() && isKeyNotWhitespace) {            boolean gotKey = TextKeyListener.getInstance().onKeyDown( mWorkspace, mDefaultKeySsb,                    keyCode, event);            if (gotKey && mDefaultKeySsb != null && mDefaultKeySsb.length() > 0) {                // something usable has been typed - start a search                // the typed text will be retrieved and cleared by                // showSearchDialog()                // If there are multiple keystrokes before the search dialog takes focus,                // onSearchRequested() will be called for every keystroke,                // but it is idempotent , so it's fine.                return onSearchRequested();            }        }        // Eat the long press event so the keyboard doesn't come up.        if (keyCode == KeyEvent.KEYCODE_MENU && event.isLongPress()) {            return true ;        }        return handled;    }
    可以看到在onKeyDown方法中,没有任何关于进出抽屉或者桌面的方法。那么再来看一下onBackPressed()方法:
  @Override    public void onBackPressed() {        if (isAllAppsVisible()) {            showWorkspace( true);        } else if (mWorkspace .getOpenFolder() != null) {            Folder openFolder = mWorkspace.getOpenFolder();            if (openFolder.isEditingName()) {                openFolder.dismissEditingName();            } else {                closeFolder();            }        } else {            mWorkspace.exitWidgetResizeMode();            // Back button is a no-op here, but give at least some feedback for the button press            mWorkspace.showOutlinesTemporarily();        }    }
 发现在这里处理了切换的过程。现在可以确定是显示桌面调用的是showWorkspace()方法,进入抽屉调用的是showAllApps()方法。在这两个方法中,又各自调用不同的方法实现各自的逻辑。   那么这两个方法都是在上面情况下调用的呢?先看showAllApps()方法。 launcher.java类: 1、onResume() 2、onClickAllAppsButton
  再看一下showWorkspace()的情况, 1、onResume() 2、mReceiver() 3、onNewIntent() 4、startSearch() 5、startWallpaper() 6、onBackPressed() 7、onClick() 8、onTouch() 9、showWorkspace() 10、exitSpringLoadedDragModeDelayed()   可以发现showAllApps()方法只有被调用两次,而showWorkspace()有十次之多。这说明返回桌面的情形比返回抽屉的情况要多很多,而实际的使用情况也确实是这样的。      接着分别看一下showAllApps()和showWorkspace()各自的具体实现。
 void showAllApps( boolean animated) {        if (mState != State.WORKSPACE) return;        showAppsCustomizeHelper(animated, false);        mAppsCustomizeTabHost.requestFocus();        // Change the state *after* we've called all the transition code        mState = State. APPS_CUSTOMIZE;        // Pause the auto-advance of widgets until we are out of AllApps        mUserPresent = false ;        updateRunning();        closeFolder();        // Send an accessibility event to announce the context change        getWindow().getDecorView()                .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);    }
   逐行分析下,第一行是一个判断,如果当前的状态不是在桌面,那么就退出这个方法。第二行代码就是进入抽屉的动画方法。 第三行代码是给抽屉界面焦点。第四行代码进入抽屉后更改当前的状态。之后就不细说了。重点还是showAppsCustomizeHelper(animated, false );这个方法,它就是实现进入抽屉动画的方法。来到这个方法,发现其上面有很长的注释:
 /**     * Things to test when changing the following seven functions.     *   - Home from workspace     *          - from center screen     *          - from other screens     *   - Home from all apps     *          - from center screen     *          - from other screens     *   - Back from all apps     *          - from center screen     *          - from other screens     *   - Launch app from workspace and quit     *          - with back     *          - with home     *   - Launch app from all apps and quit     *          - with back     *          - with home     *   - Go to a screen that's not the default, then all     *     apps, and launch and app, and go back     *          - with back     *          -with home     *   - On workspace, long press power and go back     *          - with back     *          - with home     *   - On all apps, long press power and go back     *          - with back     *          - with home     *   - On workspace, power off     *   - On all apps, power off     *   - Launch an app and turn off the screen while in that app     *          - Go back with home key     *          - Go back with back key  TODO: make this not go to workspace     *          - From all apps     *          - From workspace     *   - Enter and exit car mode (becuase it causes an extra configuration changed)     *          - From all apps     *          - From the center workspace     *          - From another workspace     */
  注释下面是 showAppsCustomizeHelper()和 hideAppsCustomizeHelper()两个方法,顾名思义 hideAppsCustomizeHelper就是离开抽屉的动画实现方法,这两个方法是相对立的。
/**     * Zoom the camera out from the workspace to reveal 'toView'.     * Assumes that the view to show is anchored at either the very top or very bottom     * of the screen.     */    private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded) {        if (mStateAnimation != null) {            mStateAnimation.setDuration(0);            mStateAnimation.cancel();            mStateAnimation = null ;        }        final Resources res = getResources();        final int duration = res.getInteger(R.integer.config_appsCustomizeZoomInTime );        final int fadeDuration = res.getInteger(R.integer.config_appsCustomizeFadeInTime );        final float scale = (float) res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor );        final View fromView = mWorkspace ;        final AppsCustomizeTabHost toView = mAppsCustomizeTabHost;        final int startDelay =                res.getInteger(R.integer.config_workspaceAppsCustomizeAnimationStagger );        setPivotsForZoom(toView, scale);        // Shrink workspaces away if going to AppsCustomize from workspace        Animator workspaceAnim =                mWorkspace.getChangeStateAnimation(Workspace.State.SMALL, animated);        if (animated) {            toView.setScaleX(scale);            toView.setScaleY(scale);            final LauncherViewPropertyAnimator scaleAnim = new LauncherViewPropertyAnimator(toView);            scaleAnim.                scaleX(1f).scaleY(1f).                setDuration(duration).                setInterpolator( new Workspace.ZoomOutInterpolator());            toView.setVisibility(View. VISIBLE);            toView.setAlpha(0f);            final ObjectAnimator alphaAnim = LauncherAnimUtils                . ofFloat(toView, "alpha", 0f, 1f)                .setDuration(fadeDuration);            alphaAnim.setInterpolator( new DecelerateInterpolator(1.5f));            alphaAnim.addUpdateListener( new AnimatorUpdateListener() {                @Override                public void onAnimationUpdate(ValueAnimator animation) {                    if (animation == null) {                        throw new RuntimeException("animation is null" );                    }                    float t = (Float) animation.getAnimatedValue();                    dispatchOnLauncherTransitionStep(fromView, t);                    dispatchOnLauncherTransitionStep(toView, t);                }            });            // toView should appear right at the end of the workspace shrink            // animation            mStateAnimation = LauncherAnimUtils.createAnimatorSet();            mStateAnimation.play(scaleAnim).after(startDelay);            mStateAnimation.play(alphaAnim).after(startDelay);            mStateAnimation.addListener(new AnimatorListenerAdapter() {                boolean animationCancelled = false;                @Override                public void onAnimationStart(Animator animation) {                    updateWallpaperVisibility( true);                    // Prepare the position                    toView.setTranslationX(0.0f);                    toView.setTranslationY(0.0f);                    toView.setVisibility(View. VISIBLE);                    toView.bringToFront();                }                @Override                public void onAnimationEnd(Animator animation) {                    dispatchOnLauncherTransitionEnd(fromView, animated, false);                    dispatchOnLauncherTransitionEnd(toView, animated, false);                    if (mWorkspace != null && !springLoaded && !LauncherApplication.isScreenLarge()) {                        // Hide the workspace scrollbar                        mWorkspace.hideScrollingIndicator(true);                        hideDockDivider();                    }                    if (!animationCancelled ) {                        updateWallpaperVisibility( false);                    }                    // Hide the search bar                    if (mSearchDropTargetBar != null) {                        mSearchDropTargetBar.hideSearchBar(false);                    }                }                @Override                public void onAnimationCancel(Animator animation) {                    animationCancelled = true ;                }            });            if (workspaceAnim != null) {                mStateAnimation.play(workspaceAnim);            }            boolean delayAnim = false;            dispatchOnLauncherTransitionPrepare(fromView, animated, false);            dispatchOnLauncherTransitionPrepare(toView, animated, false);            // If any of the objects being animated haven't been measured/laid out            // yet, delay the animation until we get a layout pass            if ((((LauncherTransitionable) toView).getContent().getMeasuredWidth() == 0) ||                    ( mWorkspace.getMeasuredWidth() == 0) ||                    (toView.getMeasuredWidth() == 0)) {                delayAnim = true;            }            final AnimatorSet stateAnimation = mStateAnimation;            final Runnable startAnimRunnable = new Runnable() {                public void run() {                    // Check that mStateAnimation hasn't changed while                    // we waited for a layout/draw pass                    if (mStateAnimation != stateAnimation)                        return;                    setPivotsForZoom(toView, scale);                    dispatchOnLauncherTransitionStart(fromView, animated, false);                    dispatchOnLauncherTransitionStart(toView, animated, false);                    LauncherAnimUtils.startAnimationAfterNextDraw( mStateAnimation, toView);                }            };            if (delayAnim) {                final ViewTreeObserver observer = toView.getViewTreeObserver();                observer.addOnGlobalLayoutListener( new OnGlobalLayoutListener() {                        public void onGlobalLayout() {                            startAnimRunnable.run();                            toView.getViewTreeObserver().removeOnGlobalLayoutListener(this);                        }                    });            } else {                startAnimRunnable.run();            }        } else {            toView.setTranslationX(0.0f);            toView.setTranslationY(0.0f);            toView.setScaleX(1.0f);            toView.setScaleY(1.0f);            toView.setVisibility(View. VISIBLE);            toView.bringToFront();            if (!springLoaded && !LauncherApplication.isScreenLarge()) {                // Hide the workspace scrollbar                mWorkspace.hideScrollingIndicator(true);                hideDockDivider();                // Hide the search bar                if (mSearchDropTargetBar != null) {                    mSearchDropTargetBar.hideSearchBar(false);                }            }            dispatchOnLauncherTransitionPrepare(fromView, animated, false);            dispatchOnLauncherTransitionStart(fromView, animated, false);            dispatchOnLauncherTransitionEnd(fromView, animated, false);            dispatchOnLauncherTransitionPrepare(toView, animated, false);            dispatchOnLauncherTransitionStart(toView, animated, false);            dispatchOnLauncherTransitionEnd(toView, animated, false);            updateWallpaperVisibility( false);        }    }
   变量mStateAnimation的类型是AnimatorSet,是专门负责上述两个方法里面的动画执行。变量duration、fadeDuration、scale分别是进入抽屉动画的伸缩的时间、透明度改变的时间以及伸缩的大小。从下面两行代码可以看出
final View fromView = mWorkspace;final AppsCustomizeTabHost toView = mAppsCustomizeTabHost ;
fromView就是桌面,而toView就是抽屉。startDelay是动画之前的准备时间。
setPivotsForZoom(toView, scale);方法是对View进行一个缩放,scale是缩放的参数。
workspaceAnim是桌面消失的动画,先不去看具体实现。如果有动画需求进入if判断,在这里有一个scaleAnim和alphaAnim,这就是抽屉出现的动画,从代码
mStateAnimation = LauncherAnimUtils.createAnimatorSet();mStateAnimation.play(scaleAnim).after(startDelay);mStateAnimation.play(alphaAnim).after(startDelay);
可以看出二者同时执行。接着是mStateAnimation动画的回调接口,具体逻辑不再分析。然后如果workspaceAnim不为空的话,就执行说明消失的动画。再看delayAnim变量,这是用来判断是否需要延迟动画执行。如果需要的话就监听View树是绘制,绘制完毕之后再执行动画;否则执行进入抽屉的动画。 还有一个重要的地方是在在上面的方法中出现了
dispatchOnLauncherTransitionPrepare(fromView, animated, false);dispatchOnLauncherTransitionStart(fromView, animated, false);dispatchOnLauncherTransitionEnd(fromView, animated, false);
等方法。其实这是在Launcher类中定义的接口里面的方法,具体如下:
interface LauncherTransitionable {     View getContent();      void onLauncherTransitionPrepare(Launcher l, boolean animated,               boolean toWorkspace);      void onLauncherTransitionStart(Launcher l, boolean animated,               boolean toWorkspace);      void onLauncherTransitionStep(Launcher l, float t);      void onLauncherTransitionEnd(Launcher l, boolean animated,               boolean toWorkspace);}
  分别在Workspace、AppsCustomizePagedView、AppsCustomizeTabHost、PagedView中继承这个接口。 最后回过头看一下桌面消失动画的实现,它是在Workspace类里面处理的。
  Animator getChangeStateAnimation (final SizeState state, boolean animated, int delay) {     Log.i(TAG, "getChangeStateAnimation");             if (mSizeState == state) {            return null ;        }        // Initialize animation arrays for the first time if necessary        initAnimationArrays();        AnimatorSet anim = animated ? LauncherAnimUtils.createAnimatorSet() : null;        // Stop any scrolling, move to the current page right away        setCurrentPage(getNextPage());        final boolean isEditViewMode = isEditViewMode(state);               final SizeState oldState = mSizeState ;        final boolean oldStateIsNormal = (oldState == SizeState.NORMAL);        final boolean oldStateIsSpringLoaded = (oldState == SizeState.SPRING_LOADED );        final boolean oldStateIsSmall = (oldState == SizeState.SMALL);        mSizeState = state;        final boolean stateIsNormal = (state == SizeState.NORMAL);        final boolean stateIsSpringLoaded = (state == SizeState.SPRING_LOADED );        final boolean stateIsSmall = (state == SizeState.SMALL);        float finalScaleFactor = 1.0f;        float finalBackgroundAlpha = (stateIsSpringLoaded || isEditViewMode) ? 1.0f: 0f;        float translationX = 0;        float translationY = 0;        boolean zoomIn = true;        if (state != SizeState.NORMAL) {               if (isEditViewMode) {                   finalScaleFactor = getCellLayoutScale(state);              } else {                   finalScaleFactor = mSpringLoadedShrinkFactor- (state == SizeState.SMALL ? 0.1f : 0);              }            finalScaleFactor = mSpringLoadedShrinkFactor - (stateIsSmall ? 0.1f : 0);            setPageSpacing(mSpringLoadedPageSpacing);            if (oldStateIsNormal && stateIsSmall) {                zoomIn = false;                setLayoutScale(finalScaleFactor);                updateChildrenLayersEnabled( false);            } else {                finalBackgroundAlpha = 1.0f;                setLayoutScale(finalScaleFactor);            }        } else {            setPageSpacing(mOriginalPageSpacing);            setLayoutScale(1.0f);        }        final int duration;        if (isEditViewMode) {              duration = getResources().getInteger(R.integer.config_overviewTransitionTime );          } else if (zoomIn) {              duration = getResources().getInteger(R.integer.config_workspaceUnshrinkTime );          } else {              duration = getResources().getInteger(R.integer.config_appsCustomizeWorkspaceShrinkTime );          }               for (int i = 0; i < getChildCount(); i++) {            final CellLayout cl = (CellLayout) getChildAt(i);            float finalAlpha = (!mWorkspaceFadeInAdjacentScreens || stateIsSpringLoaded ||(i == mCurrentPage)) ? 1f : 0f;                               float currentAlpha = cl.getShortcutsAndWidgets().getAlpha();            float initialAlpha = currentAlpha;                       // Determine the pages alpha during the state transition            if ((oldStateIsSmall && stateIsNormal) || (oldStateIsNormal && stateIsSmall)) {                // To/from workspace - only show the current page unless the transition is not                //                     animated and the animation end callback below doesn't run;                //                     or, if we're in spring-loaded mode                if (i == mCurrentPage || !animated || oldStateIsSpringLoaded) {                             finalAlpha = 1f;                } else {                    initialAlpha = 0f;                    finalAlpha = 0f;                }            }            mOldAlphas[i] = initialAlpha;            mNewAlphas[i] = finalAlpha;            if (animated) {                mOldTranslationXs[i] = cl.getTranslationX();                mOldTranslationYs[i] = cl.getTranslationY();                mOldScaleXs[i] = cl.getScaleX();                mOldScaleYs[i] = cl.getScaleY();                mOldBackgroundAlphas[i] = cl.getBackgroundAlpha();                mNewTranslationXs[i] = translationX;                mNewTranslationYs[i] = translationY;                mNewScaleXs[i] = finalScaleFactor;                mNewScaleYs[i] = finalScaleFactor;                mNewBackgroundAlphas[i] = finalBackgroundAlpha;            } else {                cl.setTranslationX(translationX);                cl.setTranslationY(translationY);                cl.setScaleX(finalScaleFactor);                cl.setScaleY(finalScaleFactor);                cl.setBackgroundAlpha(finalBackgroundAlpha);                cl.setShortcutAndWidgetAlpha(finalAlpha);            }            cl.isEditViewMode(isEditViewMode);        }        if (animated) {            for (int index = 0; index < getChildCount(); index++) {                final int i = index;                final CellLayout cl = (CellLayout) getChildAt(i);                float currentAlpha = cl.getShortcutsAndWidgets().getAlpha();                if (mOldAlphas [i] == 0 && mNewAlphas[i] == 0) {                    cl.setTranslationX(mNewTranslationXs [i]);                    cl.setTranslationY(mNewTranslationYs [i]);                    cl.setScaleX( mNewScaleXs[i]);                    cl.setScaleY( mNewScaleYs[i]);                    cl.setBackgroundAlpha(mNewBackgroundAlphas [i]);                    cl.setShortcutAndWidgetAlpha(mNewAlphas [i]);                    cl.setRotationY( mNewRotationYs[i]);                } else {                    LauncherViewPropertyAnimator a = new LauncherViewPropertyAnimator(cl);                    a.translationX( mNewTranslationXs[i])                        .translationY(mNewTranslationYs [i])                        .scaleX( mNewScaleXs[i])                        .scaleY( mNewScaleYs[i])                        .setDuration(duration)                        .setInterpolator(mZoomInInterpolator );                    anim.play(a);                    if (mOldAlphas [i] != mNewAlphas [i] || currentAlpha != mNewAlphas [i]) {                        LauncherViewPropertyAnimator alphaAnim =                            new LauncherViewPropertyAnimator(cl.getShortcutsAndWidgets());                        alphaAnim.alpha( mNewAlphas[i])                            .setDuration(duration)                            .setInterpolator(mZoomInInterpolator );                        anim.play(alphaAnim);                    }                    if (mOldBackgroundAlphas [i] != 0 ||                        mNewBackgroundAlphas[i] != 0) {                        ValueAnimator bgAnim =                                LauncherAnimUtils.ofFloat(cl, 0f, 1f).setDuration(duration);                        bgAnim.setInterpolator(mZoomInInterpolator );                        bgAnim.addUpdateListener( new LauncherAnimatorUpdateListener() {                                public void onAnimationUpdate(float a, float b) {                                    cl.setBackgroundAlpha(                                            a * mOldBackgroundAlphas[i] +                                            b * mNewBackgroundAlphas[i]);                                }                            });                        anim.play(bgAnim);                    }                }            }            anim.setStartDelay(delay);        }        if (stateIsSpringLoaded) {            // Right now we're covered by Apps Customize            // Show the background gradient immediately, so the gradient will            // be showing once AppsCustomize disappears            animateBackgroundGradient(getResources().getInteger(                    R.integer.config_appsCustomizeSpringLoadedBgAlpha ) / 100f, false);        } else {            // Fade the background gradient away            animateBackgroundGradient(0f, true);        }        return anim;    }
  在说明方法之前先说一下,Launcher2和Launcher3桌面消失动画的区别。Launcher2进入抽屉后是一个黑色的背景,没有壁纸显示效果,而Launcher3可以看到壁纸。所以getChangeStateAnimation 方法也是有一定区别的。关于这个方法大体可以分为三个部分,一是变量初始赋值阶段,二是动画设置阶段,三是动画返回阶段。重点是第二个动画设置阶段,因为桌面的消失、出现都会调用这个方法。同样的关于这个动画最主要的部分还是伸缩和透明度的变化,理论上应该是这样的,桌面消失时开始变小、透明度逐渐不可见;桌面出现时开始变大、透明度逐渐可见。
  不过从这个方法的实现来看,其性能消耗比较大。它是用一个for循环来遍历Workspace的CellLayout的个数,也就是有几个桌面。然后再对每个CellLayout进行一个动画效果。亲测在低端机器上比如联想s868t是非常卡顿的。
  hideAppsCustomizeHelper()的实现就不再赘述,和showAppsCustomizeHelper()实现机制都是一样的。那么最后总结一下,从桌面进入抽屉调用
hideAppsCustomizeHelper()方法,这个方法实现进入抽屉的动画效果,具体说就是对抽屉这个View进行一个动画。在这方法里面还有一个桌面消失的动画getChangeStateAnimation ,在这个动画执行完毕后才会执行进入抽屉的动画。然后从抽屉返回桌面正好是相反的流程。现在很多桌面进入抽屉都会有其他的动画效果,比如反转、上下等效果,其实就是对以上三个方法进行修改添加就可以了。











更多相关文章

  1. Android 4.4 Dialog 被状态栏遮挡的解决方法
  2. android系统裁剪方法
  3. DIY osc android 客户端 之 方法论
  4. 【Android - 基础】之Animator属性动画
  5. android中的颜色渐变动画---可用于导航页的效果
  6. Android 动画分析之属性动画
  7. LayoutTransition 容器布局动画

随机推荐

  1. 使用AIDL实现进程间的通信
  2. 让App吐出自己的Crash信息
  3. AndroidN新增物理按键[android7.1.2][msm
  4. Android手机APP测试之环境搭建
  5. Android(安卓)调用相机拍照并显示,打开相
  6. Android(安卓)Studio建立Socket连接失败
  7. 2018年Android面试题汇总四(持续更新中)
  8. Android开发学习笔记——对话框Dialog
  9. Android系统版本及其屏幕适配
  10. Android(安卓)Fragment 基本了解