寻寻觅觅终于等到你,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/