开源项目:BottomBar

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/AnalyzeSystem/article/details/51122653

前言

寻寻觅觅终于等到你,Material Design系列BottomBar开源库你值得拥有。从我接触android开发遇到tabhost,到radioGroup+ViewPage/FrameLayout的演变,再到官方重做tabhost,纵观历史演变,淡看风云变幻,我心依旧,BottomBar你一直都是我的唯一!!


运行效果图

调用实例

as项目导入(需要注意该库的sdk限制: minSdkVersion 11)

compile 'com.roughike:bottom-bar:1.3.2'

① 通过添加menu>item资源

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/bottomBarItemOne"
        android:icon="@drawable/ic_recents"
        android:title="Recents" />
        ...
</menu>

* ② Activity内调用我们需要先保存我们的BottomBar状态,同时也要恢复BottomBar的状态,具体做法如下:*


     //将BottomBar绑定到你的活动,抬高你的布局。
     mBottomBar = BottomBar.attach(this, savedInstanceState);

    //...................................

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        // 需要恢复BottomBar的状态
        mBottomBar.onSaveInstanceState(outState);
    }

③ 设置不同选项卡对应不同的颜色,Rcolor.colorAccent系统默认主题相关的控件颜色

  mBottomBar.mapColorForTab(0, ContextCompat.getColor(this, R.color.colorAccent));
  mBottomBar.mapColorForTab(1, 0xFF5D4037);
  mBottomBar.mapColorForTab(2, "#7B1FA2");
  mBottomBar.mapColorForTab(3, "#FF5252");
  mBottomBar.mapColorForTab(4, "#FF9800");

④ 对于MenuItem选中的监听设置自定义的接口OnMenuTabClickListener

  mBottomBar.setItemsFromMenu(R.menu.bottombar_menu, new OnMenuTabClickListener() {
            @Override
            public void onMenuTabSelected(@IdRes int menuItemId) {

            }

            @Override
            public void onMenuTabReSelected(@IdRes int menuItemId) {

            }
        });

⑤ 如果BottomBar的功能仅此而已还不值得我为此点赞,导航Tab在Im通信领域的消息count显示,BottomBar也为我们做了很好地实现

       //tab对应消息count以及颜色设定
        BottomBarBadge unreadMessages = mBottomBar.makeBadgeForTabAt(0, "#FF0000", 13);
       // 控制显示与否
        unreadMessages.show();
       // unreadMessages.hide();

       // 动态单独改变现实个数
        unreadMessages.setCount(4);

       // 改变显示隐藏的动画时间
        unreadMessages.setAnimationDuration(200);

      // 是否没有选中也要现实消息
        unreadMessages.setAutoShowAfterUnSelection(true);

⑥ BottomBar还提供了定制化开发

       //禁用左侧边导航
        mBottomBar.noTabletGoodness();

       // 显示所有标题即使有超过三个选项卡。
        mBottomBar.useFixedMode();

        // 使用黑暗的主题。
        mBottomBar.useDarkTheme();

       // 为活动选项卡设置颜色。忽略了在移动时超过三个选项卡。
        mBottomBar.setActiveTabColor("#009688");

        // 使用自定义文本出现在选项卡相关配置。
        mBottomBar.setTextAppearance(R.style.MyTextAppearance);

       // 设置assets目录下的字体
        mBottomBar.setTypeFace("MyFont.ttf");

⑦ BottomBar在界面发生滑动的时候可以把他隐藏,在滑动结束后在显示出来,不过这样就需要改变BottomBar保存状态的方法

 mBottomBar = BottomBar.attachShy((CoordinatorLayout) findViewById(R.id.myCoordinator), 
    findViewById(R.id.myScrollingContent), savedInstanceState);

而xml文件这里使用CoordinatorLayout为例已对照上列代码

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/myCoordinator"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/myScrollingContent"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Your loooong scrolling content here -->

    </android.support.v4.widget.NestedScrollView>

</android.support.design.widget.CoordinatorLayout>

⑧ BottomBar除了可以通过Menu xml文件导入,我们还可以通过代码的方式导入项目

mBottomBar.setItems(
  new BottomBarTab(R.drawable.ic_recents, "Recents"),
  new BottomBarTab(R.drawable.ic_favorites, "Favorites"),
  new BottomBarTab(R.drawable.ic_nearby, "Nearby")
);

// Listen for tab changes
mBottomBar.setOnTabClickListener(new OnTabClickListener() {
    @Override
    public void onTabSelected(int position) {
        // The user selected a tab at the specified position
    }

    @Override
    public void onTabReSelected(int position) {
        // The user reselected a tab at the specified position!
    }
});

⑨ BottomBar虽然不能用xml直接布局,但你仍然可以把它放在任何地方的视图层次。只要把它绑定到任何你想要的任何视图位置:

mBottomBar.attach(findViewById(R.id.myContent), savedInstanceState);

⑩ 如果你觉得透明的底部导航让你觉得不开心,你可以禁用它,但是你必须得注意,该方法必须在填充Item前调用否则会抛出异常(更多使用方法请参照API自行了解)

 mBottomBar.noNavBarGoodness();

源码分析

看完上面的使用简介,我们再来细嚼慢咽品源码,我一直相信多学习别人的开源项目我们可以收获很多,今天一定会斩获良多。废话不多说,先看开源项目的结构目录,我们层层渗入。

  • BadgeCircle
  • BottomBar
  • BottomBarBadge
  • BottomBarFragment
  • BottomBarItemBase
  • BottomBarTab
  • MisicUtils
  • OnMenuTabClickListener
  • OnSizeDeterminedListener
  • OnTabClickListener
  • OnTabSelectedListener
  • BottomNavigationBehavior
  • VerticalScrollingBehavior

① BadgeCircle辅助类创建一个圆形背景图,涉及到知识点Drawable系列,shape直接子类OvalShape,而Drawable子类ShapeDrawable构造函数传入OvaShape实例化创建指定大小颜色的背景图片。


public class BadgeCircle {
    /**
     * Creates a new circle for the Badge background.
     *
     * @param size  the width and height for the circle
     * @param color the color for the circle
     * @return a nice and adorable circle.
     */
    protected static ShapeDrawable make(int size, int color) {
        ShapeDrawable indicator = new ShapeDrawable(new OvalShape());
        indicator.setIntrinsicWidth(size);
        indicator.setIntrinsicHeight(size);
        indicator.getPaint().setColor(color);
        return indicator;
    }
}

② 在了解其他类之前我们得先来看看MiscUtils工具类

class MiscUtils {

    /**
     * 获取主题颜色
     */
    protected static int getColor(Context context, int color) {
        TypedValue tv = new TypedValue();
        context.getTheme().resolveAttribute(R.attr.colorPrimary, tv, true);
        return tv.data;
    }

    /**
     * Converts dps to pixels nicely.
     * dp转px
     * @param context the Context for getting the resources
     * @param dp      dimension in dps
     * @return dimension in pixels
     */
    protected static int dpToPixel(Context context, float dp) {
        Resources resources = context.getResources();
        DisplayMetrics metrics = resources.getDisplayMetrics();
        return (int) (dp * (metrics.densityDpi / 160f));
    }

    /**
     * Returns screen width.
     * 获取屏幕宽度
     * @param context Context to get resources and device specific display metrics
     * @return screen width
     */
    protected static int getScreenWidth(Context context) {
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        return (int) (displayMetrics.widthPixels / displayMetrics.density);
    }

    /**
     * A hacky method for inflating menus from xml resources to an array
     * of BottomBarTabs.
     * 从Menu引入BottomBarTab[]资源
     * @param activity the activity context for retrieving the MenuInflater.
     * @param menuRes  the xml menu resource to inflate
     * @return an Array of BottomBarTabs.
     */
    protected static BottomBarTab[] inflateMenuFromResource(Activity activity, @MenuRes int menuRes) {
        // A bit hacky, but hey hey what can I do
        PopupMenu popupMenu = new PopupMenu(activity, null);
        Menu menu = popupMenu.getMenu();
        activity.getMenuInflater().inflate(menuRes, menu);

        int menuSize = menu.size();
        BottomBarTab[] tabs = new BottomBarTab[menuSize];

        for (int i = 0; i < menuSize; i++) {
            MenuItem item = menu.getItem(i);
            BottomBarTab tab = new BottomBarTab(item.getIcon(),
                    String.valueOf(item.getTitle()));
            tab.id = item.getItemId();
            tabs[i] = tab;
        }

        return tabs;
    }

    /**
     * A method for animating width for the tabs.
     * 执行该动画通过LayoutParams动态改变BottomTabs的宽高
     * @param tab tab to animate.
     * @param start starting width.
     * @param end final width after animation.
     */
    protected static void resizeTab(final View tab, float start, float end) {
        ValueAnimator animator = ValueAnimator.ofFloat(start, end);
        animator.setDuration(150);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
                ViewGroup.LayoutParams params = tab.getLayoutParams();
                if (params == null) return;

                /***
                 * 1. Math.ceil()用作向上取整。
                 * 2. Math.floor()用作向下取整。
                 * 3. Math.round() 我们数学中常用到的四舍五入取整。
                 */
                params.width = Math.round((float) animator.getAnimatedValue());
                tab.setLayoutParams(params);
            }
        });
        animator.start();
    }

    /**
     * Animate a background color change. Uses Circular Reveal if supported,
     * otherwise crossfades the background color in.
     * 设备支持(API21)圆形扩散波纹,就用这种方式改变背景,否则就通过淡入淡出背景色的方式
     * 
     * 触摸点击view
     * @param clickedView    the view that was clicked for calculating the start position for the Circular Reveal.
     * 当前展示的背景色                      
     * @param backgroundView the currently showing background color.
     * 覆盖后的背景色
     * @param bgOverlay      the overlay view for the new background color that will be
     *                       animated in.
     * 新的颜色
     * @param newColor       the new color.
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    protected static void animateBGColorChange(View clickedView, final View backgroundView,
                                               final View bgOverlay, final int newColor) {
        int centerX = (int) (ViewCompat.getX(clickedView) + (clickedView.getMeasuredWidth() / 2));
        int centerY = clickedView.getMeasuredHeight() / 2;
        int finalRadius = backgroundView.getWidth();

        backgroundView.clearAnimation();
        bgOverlay.clearAnimation();

        Object animator;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (!bgOverlay.isAttachedToWindow()) {
                return;
            }
            // api 21 后引入圆形缩放动画效果,效果图如下图
            animator = ViewAnimationUtils
                    .createCircularReveal(bgOverlay, centerX, centerY, 0, finalRadius);
        } else {
           //如果是低版本的仅仅透明度的变化
            ViewCompat.setAlpha(bgOverlay, 0);
            animator = ViewCompat.animate(bgOverlay).alpha(1);
        }

        if (animator instanceof ViewPropertyAnimatorCompat) {
            ((ViewPropertyAnimatorCompat) animator).setListener(new ViewPropertyAnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(View view) {
                    onCancel();
                }

               //******************此处略*******************

        bgOverlay.setBackgroundColor(newColor);
        bgOverlay.setVisibility(View.VISIBLE);
    }

    /**
     * A convenience method for setting text appearance.
     * 一种设置文本的方便方法,通过传入文本相关配置对应的资源id
     * @param textView a TextView which textAppearance to modify.
     * @param resId    a style resource for the text appearance.
     */
    @SuppressWarnings("deprecation")
    protected static void setTextAppearance(TextView textView, int resId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            textView.setTextAppearance(resId);
        } else {
            textView.setTextAppearance(textView.getContext(), resId);
        }
    }

    /**
     * Determine if the current UI Mode is Night Mode.
     *
     * @param context Context to get the configuration.
     * @return true if the night mode is enabled, otherwise false.
     * 判断是否是夜间模式
     */
    protected static boolean isNightMode(Context context) {
        int currentNightMode = context.getResources().getConfiguration().uiMode
                & Configuration.UI_MODE_NIGHT_MASK;
        return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
    }
}

以上Utils有着我们不得不了解的知识点(当然如果你已经知道了可以略过)ViewAnimationUtils.createCircularReveal方法创建圆形扩散波纹,该方法在API21引入,效果如下图

如果要在低版本实现上图效果,具体采用办法请参考http://www.cnblogs.com/linguanh/p/4610174.html?utm_source=tuicool&utm_medium=referral

③ Tab导航添加消息count显示,这里用到的是自定义TextView控件 BottomBarBadge,内部实现setCount重新调用setText赋值,hide、 show方法实现控件自身的隐藏和显示(本质是缩放0-1),还提供了一个属性autoShowAfterUnSelection,对外公开get set,以便于外部判断调用

    /**
     * Controls whether you want this Badge to be shown automatically when the
     * BottomBar tab containing it is unselected.
     * 设置Tab没有被选中时,是否显示该控件,默认不显示
     * @param autoShowAfterUnSelection false if you don't want to this Badge reappear every time
     *                                 the BottomBar tab containing it is unselected.
     */
    public void setAutoShowAfterUnSelection(boolean autoShowAfterUnSelection) {
        this.autoShowAfterUnSelection = autoShowAfterUnSelection;
    }

我们再来了解一下内部构造方法具体实现


    protected BottomBarBadge(Context context, int position, final View tabToAddTo, // Rhyming accidentally! That's a Smoove Move!
                             int backgroundColor) {
        super(context);

        ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);

        setLayoutParams(params);
        setGravity(Gravity.CENTER);
        MiscUtils.setTextAppearance(this,
                R.style.BB_BottomBarBadge_Text);

        int three = MiscUtils.dpToPixel(context, 3);
        //设置消息背景
        ShapeDrawable backgroundCircle = BadgeCircle.make(three * 3, backgroundColor);
        setPadding(three, three, three, three);
        //分支适配设置drawable
        setBackgroundCompat(backgroundCircle);

        FrameLayout container = new FrameLayout(context);
        container.setLayoutParams(params);

        //先移除child 重新build后重新添加,并添加OnGlobalLayoutListener,从而达到调整位置和大小的目的
        ViewGroup parent = (ViewGroup) tabToAddTo.getParent();
        parent.removeView(tabToAddTo);

        container.setTag(tabToAddTo.getTag());
        container.addView(tabToAddTo);
        container.addView(this);

        parent.addView(container, position);

        container.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @SuppressWarnings("deprecation")
            @Override
            public void onGlobalLayout() {
                adjustPositionAndSize(tabToAddTo);
            }
        });

④ BottomBarItemBase对象用户配置BottomBar的基本属性,比如文字、图片。内部提供方法没什么特别的,不过还是让我有所发现ContextCompat.getDrawable(Context mContext,int id)方法,以前我都用getDrawabale(id)然而有版本兼容问题,要走分支getDrawable(Context,id),而这部分代码以前都是自己手写,当我发现了Compat系列的ContextCompat,一切都变得简单了,相信很多类似的Compat类都有很多很不错的方法实现,空余时间可以多看看

    /**
     * Return a drawable object associated with a particular resource ID.
     * <p>
     * Starting in {@link android.os.Build.VERSION_CODES#LOLLIPOP}, the returned
     * drawable will be styled for the specified Context's theme.
     *
     * @param id The desired resource identifier, as generated by the aapt tool.
     *            This integer encodes the package, type, and resource entry.
     *            The value 0 is an invalid identifier.
     * @return Drawable An object that can be used to draw this resource.
     */
    public static final Drawable getDrawable(Context context, int id) {
        final int version = Build.VERSION.SDK_INT;
        if (version >= 21) {
            return ContextCompatApi21.getDrawable(context, id);
        } else {
            return context.getResources().getDrawable(id);
        }
    }

⑥ BottomBarTab只是对于BottomBarItemBase的继承,没做跟多的操作,修改了多种创建方式,这里不作多介绍了

由浅入深,我们接着来看看这些接口定义到底有什么作用,当然OnMenuTabSelectedListener、OnTabSelectedListener以及BottomBarFragment这些过时类就不了解了,顺便提一句,让类或者方法过时直接在其上面添加注解@Deprecated即可。(这个开源项目废弃已有的接口原因在于Tab的重复选择监听)

/**
 * updateSelectedTab()方法内部通过notifyRegularListener()进行回调
 **/
public interface OnTabClickListener {
    /**
     * The method being called when currently visible {@link BottomBarTab} changes.
     * BottomBarTab方式被引入,当前Tab被选中
     * This listener is fired for the first time after the items have been set and
     * also after a configuration change, such as when screen orientation changes
     * from portrait to landscape.
     *
     * @param position the new visible {@link BottomBarTab}
     */
    void onTabSelected(int position);

    /**
     * The method being called when currently visible {@link BottomBarTab} is
     * reselected. Use this method for scrolling to the top of your content,
     * as recommended by the Material Design spec
     * BottomBarTab方式被引入,当前Tab被重新选中
     * @param position the {@link BottomBarTab} that was reselected.
     */
    void onTabReSelected(int position);
}

/**
 * updateSelectedTab()方法内部通过notifyMenuListener()进行回调
 **/ 
public interface OnMenuTabClickListener {
    /**
     * The method being called when currently visible {@link BottomBarTab} changes.
     *
     * This listener is fired for the first time after the items have been set and
     * also after a configuration change, such as when screen orientation changes
     * from portrait to landscape.
     * Menu布局xml方式被引入,第一次选中
     * @param menuItemId the new visible tab's id that
     *                   was assigned in the menu xml resource file.
     */
    void onMenuTabSelected(@IdRes int menuItemId);

    /**
     * The method being called when currently visible {@link BottomBarTab} is
     * reselected. Use this method for scrolling to the top of your content,
     * as recommended by the Material Design spec
     * Menu布局xml方式被引入,重新选中
     * @param menuItemId the reselected tab's id that was assigned in the menu
     *                   xml resource file.
     */
    void onMenuTabReSelected(@IdRes int menuItemId);
}

漫长的篇幅还没看到核心部位,请君息怒!!BottomBar告诉我,我们必须的在了解Behavior才行,Behavior是位于CoordinatorLayout下面的抽象类,先开始我们的解读CoordinatorLayout之旅,从该开源项目结构目录发现,用到了Nested系列的知识,so 我们必须了解下面这几个类:NestedScrollingParentHelper 、NestedScrollingParent 、NestedScrollingChildHelper、NestedScrollingChild

NestedScrollingParent接口定义解读:

/**
 * 这个接口应该实现由{ @link android.view。ViewGroup ViewGroup }子类希望支持滚动操作委托由一个嵌套的子
 * 实现类内部调用了ViewCompat 、ViewGroupCompat的静态方法(版本分支兼容) ,这样可以确保与嵌套滚动视图在5.0 + - 的兼容
 **/
public interface NestedScrollingParent {
    /**
     * 该方法表示滑动开始的调用,直到滑动结束调用onStopNestedScroll方法的调用
     * @param child 当前ViewGroup直接的子View
     * @param 开始嵌套滚动的视图View
     * @param 需要嵌套滚动的轴:水平、垂直{@link ViewCompat#SCROLL_AXIS_HORIZONTAL},{@link ViewCompat#SCROLL_AXIS_VERTICAL} or both
     * @return true 如果这个ViewParent接受嵌套滚动操作返回boolean值true
     */
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    /**
     * 如果onStartNestedScroll(View, View, int) onStartNestedScroll} returns true.则会调用该方法,表示接受了嵌套滚动
     */
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    /**
     *  MotionEvent.ACTION_UP or MotionEvent.ACTION_CANCEL表示滑动结束,回调该函数
     */
    public void onStopNestedScroll(View target);

    /**
     * 嵌套滑动进度,
     *
     * @param target The descendent view controlling the nested scroll
     * @param dxConsumed 已经水平滚动了得距离
     * @param dyConsumed 已经垂直滚动了得距离
     * @param dxUnconsumed 水平还能滚动的距离
     * @param dyUnconsumed 垂直还能滚动的距离
     */
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    /**
     * 每次滑动前,Child 先询问 Parent 是否需要滑动,即 dispatchNestedPreScroll(),
     * 这就回调到 Parent 的 onNestedPreScroll(),在这里可以拦截child的滑动
     *
     * @param target View that initiated the nested scroll
     * @param dx 水平滚动距离
     * @param dy 垂直滚动距离
     * @param consumed Output. The horizontal and vertical scroll distance consumed by this parent
     */
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    /**
     * 当进行fling滑动时回调
     * 
     * @param target View that initiated the nested scroll
     * @param velocityX 水平滑动速度
     * @param velocityY 垂直滑动速度
     * @param consumed true if the child consumed the fling, false otherwise
     * @return true if this parent consumed or otherwise reacted to the fling
     */
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    /**
     * 处理fling动作的,我们在滑动松开手的时候,视图还继续滑动一会,这种效果onNestedPreFling就派上用场了
     * @param target View that initiated the nested scroll
     * @param velocityX 水平滑动速度
     * @param velocityY 垂直滑动速度
     * @return true if this parent consumed the fling ahead of the target view
     */
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    /**
     * 获取当前滑动的方向
     * @see ViewCompat#SCROLL_AXIS_HORIZONTAL
     * @see ViewCompat#SCROLL_AXIS_VERTICAL
     * @see ViewCompat#SCROLL_AXIS_NONE
     */
    public int getNestedScrollAxes();
}

NestedScrollingChild接口定义解读


/**
 * 支持嵌套滚动调度
 * 方法处理基本由NestedScrollingChildHelper代理
 */
public interface NestedScrollingChild {
    /**
     * 启用或禁用嵌套滚动视图。
     * 如果这个属性被设置为true视图将允许嵌套滚动操作与兼容的父视图在当前的层次结构。
     * 如果这视图没有实现嵌套滚动这将没有影响。
     *
     * @see #isNestedScrollingEnabled()
     */
    public void setNestedScrollingEnabled(boolean enabled);


    public boolean isNestedScrollingEnabled();

    /**
     * 开始嵌套滚动,传入嵌套滚动方向
     * 告诉 Parent,你要准备进入滑动状态了,调用startNestedScroll()。
     */
    public boolean startNestedScroll(int axes);

    /**
     * 停止嵌套滚动
     */
    public void stopNestedScroll();

    /**
     * 该嵌套滚动视图是否有父布局.
     */
    public boolean hasNestedScrollingParent();

    /**
     * 派遣嵌套滚动视图的滚动进度
     * 如果父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距离余量。
     * 然后,你自己进行余下的滑动。最后,如果滑动距离还有剩余,你就再问一下,Parent
     * 是否需要在继续滑动你剩下的距离,也就是调用dispatchNestedScroll()。
     *
     * @param dxConsumed     水平滚动距离
     * @param dyConsumed     垂直滚动距离
     * @param dxUnconsumed   水平还能滚动的距离
     * @param dyUnconsumed   垂直还能滚动的距离
     * @param offsetInWindow 可选项
     */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                                        int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    /**
     * 在滑动之前,先问一下你的 Parent 是否需要滑动,也就是调用dispatchNestedPreScroll()。
     *
     * @param dx
     * @param dy
     * @param consumed
     * @param offsetInWindow View的窗体偏移量
     */
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    /***************略***************

}

NestedScrollingParentHelper类内部方法的实现主要在改变变量:mViewGroup、mNestedScrollAxes,不做过多解释,NestedScrollingChildHelper对NestedScrollingChild接口的代理实现,方法的实现主要以Compat系列的静态方法调用为主,ViewCompatImpl、ViewParentCompatImpl根据SDK对应不同的实现

自定义抽象类VerticalScrollingBehavior内部主要注解了滑动方向重写父类方法,修改滑动方向

public abstract class VerticalScrollingBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {

 @Retention(RetentionPolicy.SOURCE)
 @IntDef({ScrollDirection.SCROLL_DIRECTION_UP, ScrollDirection.SCROLL_DIRECTION_DOWN})
 public @interface ScrollDirection {
        int SCROLL_DIRECTION_UP = 1;
        int SCROLL_DIRECTION_DOWN = -1;
        int SCROLL_NONE = 0;
 }

 @Override
 public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int    dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        if (dyUnconsumed > 0 && mTotalDyUnconsumed < 0) {
            mTotalDyUnconsumed = 0;
            mOverScrollDirection = ScrollDirection.SCROLL_DIRECTION_UP;
        } else if (dyUnconsumed < 0 && mTotalDyUnconsumed > 0) {
            mTotalDyUnconsumed = 0;
            mOverScrollDirection = ScrollDirection.SCROLL_DIRECTION_DOWN;
        }
        mTotalDyUnconsumed += dyUnconsumed;
        onNestedVerticalOverScroll(coordinatorLayout, child, mOverScrollDirection, dyConsumed, mTotalDyUnconsumed);
    }

 @Override
 public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        if (dy > 0 && mTotalDy < 0) {
            mTotalDy = 0;
            mScrollDirection = ScrollDirection.SCROLL_DIRECTION_UP;
        } else if (dy < 0 && mTotalDy > 0) {
            mTotalDy = 0;
            mScrollDirection = ScrollDirection.SCROLL_DIRECTION_DOWN;
        }
        mTotalDy += dy;
        onDirectionNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, mScrollDirection);
    }


 @Override
 public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY, boolean consumed) {
        super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
        mScrollDirection = velocityY > 0 ? ScrollDirection.SCROLL_DIRECTION_UP : ScrollDirection.SCROLL_DIRECTION_DOWN;
        return onNestedDirectionFling(coordinatorLayout, child, target, velocityX, velocityY, mScrollDirection);
    }
}

VerticalScrollingBehavior 的具体实现类BottomNavigationBehavior,根据滑动是调用handleDirection方法判断是否可以滑动以及滑动方向,最后调用animateOffset动画实现位移translationY

public class BottomNavigationBehavior<V extends View> extends VerticalScrollingBehavior<V> {
 @Override
 public void onDirectionNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed, @ScrollDirection int scrollDirection) {
        handleDirection(child, scrollDirection);
    }

 private void handleDirection(V child, int scrollDirection) {
        if (!mScrollingEnabled) return;
        if (scrollDirection == ScrollDirection.SCROLL_DIRECTION_DOWN && hidden) {
            hidden = false;
            animateOffset(child, mDefaultOffset);
        } else if (scrollDirection == ScrollDirection.SCROLL_DIRECTION_UP && !hidden) {
            hidden = true;
            animateOffset(child, mBottomNavHeight + mDefaultOffset);
        }
    }

 @Override
 protected boolean onNestedDirectionFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY, @ScrollDirection int scrollDirection) {
        handleDirection(child, scrollDirection);
        return true;
    }

 private void animateOffset(final V child, final int offset) {
        ensureOrCancelAnimator(child);
        mTranslationAnimator.translationY(offset).start();
    }

 private void ensureOrCancelAnimator(V child) {
        if (mTranslationAnimator == null) {
            mTranslationAnimator = ViewCompat.animate(child);
            mTranslationAnimator.setDuration(300);
            mTranslationAnimator.setInterpolator(INTERPOLATOR);
        } else {
            mTranslationAnimator.cancel();
        }
    }

}

BottomBar内部源码篇幅太长,提炼出一下几点核心方法,具体代码实现自己查看源码吧一眼看穿就不做过多解释了

① clearItems();

② updateItems(mItems);

③ unselectTab(oldTab, animate);

④ selectTab(newTab, animate);

⑤ updateSelectedTab(position);

⑥ shiftingMagic(oldTab, newTab, false);

⑦ setDefaultTabPosition()

⑧ setBarVisibility()

⑨ useDarkTheme()

⑩ notifyMenuListener()

⑪ notifyRegularListener()


参考资料
http://www.race604.com/android-nested-scrolling/

猜你喜欢

转载自blog.csdn.net/AnalyzeSystem/article/details/51122653