Material Design系列,自定义Behavior实现Android知乎首页

    Material Design系列,自定义Behavior实现Android知乎首页

    版权声明:转载必须注明本文转自严振杰的博客: http://blog.csdn.net/yanzhenjie1003

    友情连接:
    Material Design博客专栏

    系列博客:
    1. Material Design系列,Behavior之BottomSheetBehavior与BottomSheetDialog
    2. Material Design系列,Behavior之SwipeDismissBehavior
    3. Material Design系列,自定义Behavior之上滑显示返回顶部按钮
    4. Material Design系列,自定义Behavior实现Android知乎首页
    5. Material Design系列,自定义Behavior支持所有 View

    本博客目的:仿知乎首页向上滑动时动画隐藏ToolbarFlocationActionButtonTab导航,下滑时显示,如果和你的期望不同,那么你可以不需要看了,免的浪费你的宝贵时间噢。

    效果预览

    知乎效果:
    效果预览
    本博客实现效果:
    效果预览

    今天效果的源代码下载链接在文章末尾。


    实现分析

    这个效果其实并不难实现,但是它的用处很大,当用户手指上滑,屏幕上显示下方内容的时候,隐藏ToolbarTab导航FAB来腾出更大的空间显示内容,让用户爽。简单粗暴,但这就是我们的目的。

    首先就是头部的Toolbar,这个就不用说了吧,基本会,不会的人随便看我一篇博客的demo都有这个效果,简直小学级别。

    其次来看看FAB(FlocationActionButton)的显示和隐藏,知乎是用的平移,我们这里做个优化改动,当然平移也是可以的,如果你看过我的Material Design系列,自定义Behavior之上滑显示返回顶部按钮这篇博客的话。那么我们的FAB的动画隐藏和显示也是用上一篇博客的原理,没有看上一篇博客的同学需要回过头看看噢,这里不在赘述。

    最后来看下面的Tab导航的隐藏和显示,这个确确实实用平移更好是吧,然而相信你如果看过我Material Design系列,Behavior之BottomSheetBehavior与BottomSheetDialog这篇博客的话,这个效果实现起来也不难。强烈建议看下文之前读这篇文章,不然真的没法继续看下去了。

    其实代码量还是很少的,主要是Behavior原理、BehaviorCoordinatorLayout如何结合使用。so,强烈建议去上读下上面两篇博客噢。

    ……

    好的,五分钟过去了,我相信你大概已经速读了上面提到的两篇博客了。那么在第一篇FAB的那篇博客中实现的效果是手指向上滑时(屏幕显示下方的内容时)显示FAB用来回到顶部,但是这里刚好是相反的:向上滑时隐藏FAB。如果你认真读了原理解释的那一段或者运行过demo,这个效果so easy吧。第二篇博客中也讲到了如何隐藏和显示这个Tab导航,那么有的同学就觉得今天的博客就结束了吧?答案当然是No了,不然我也不会再开一篇博客来讲这个了。

    为什么呢?还是有难点的,难点在哪里?就是上面讲到的两个Behavior如何和CoordinatorLayout结合使用,同时实现两个效果。而且BottomSheetBehavior隐藏和显示Tab导航这个里面之前我们使用Button来控制的,如何做到`CoordinatorLayout中的ContentView滑动时动态的显示和隐藏Tab导航呢?

    撸码

    闲扯装逼都弄完了,下来来点真材实料,带领大家一起代码撸起来。

    页面布局

    上面的引文和介绍,我们已经知道了FAB的显示和隐藏用自定义Behavior实现,Tab导航BottomSheetBehavior来实现,那么我们布局文件也该问世了:

    <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/AppTheme.AppBarOverlay">
    
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_scrollFlags="scroll|enterAlways|snap"
                app:popupTheme="@style/AppTheme.PopupOverlay" />
        </android.support.design.widget.AppBarLayout>
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior" />
    
        <LinearLayout
            android:id="@+id/tab_layout"
            android:layout_width="match_parent"
            android:layout_height="?actionBarSize"
            android:layout_alignParentBottom="true"
            android:background="@android:color/white"
            app:layout_behavior="@string/bottom_sheet_behavior">
    
            <Button
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:text="第一" />
    
            <Button
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:text="第二" />
    
            <Button
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:text="第三" />
    
            <Button
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:text="第四" />
        </LinearLayout>
    
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_marginBottom="70dp"
            android:layout_marginEnd="16dp"
            android:layout_marginRight="16dp"
            android:src="@mipmap/ic_action_new"
            app:layout_behavior="@string/scale_down_show_behavior"
            app:layout_scrollFlags="scroll|enterAlways|snap" />
    </android.support.design.widget.CoordinatorLayout>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    还是稍微解释下,内容区域是一个RecyclerView,使用的Behavior是design的ScrollingViewBehavior

    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    • 1

    然后一个LinearLayout,使用的Behavior是design的BottomSheetBehavior

    app:layout_behavior="@string/bottom_sheet_behavior"
    • 1

    最后一个FloatingActionButton,使用我们的自定义ScaleDownShowBehavior

    app:layout_behavior="@string/scale_down_show_behavior"
    • 1

    其他两个都是design自带的,唯有FloatingActionButtonScaleDownShowBehavior需要我们自定义,那么下面我们就来实现下ScaleDownShowBehavior

    自定义Behavior实现FAB的动画控制

    这里又谈到了自定义Behavior了,首先就来实现:用户手指在屏幕上滑,隐藏FAB,留出更多位置给用户。

    这里还是继承FloatingActionButton.Behavior

    public class ScaleDownShowBehavior extends FloatingActionButton.Behavior {
        public ScaleDownShowBehavior(Context context, AttributeSet attrs) {
            super();
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5

    这里我们的滑动方向还是不变,监听竖着方向的滑动:

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, ...) {
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
    }
    • 1
    • 2
    • 3
    • 4

    那么我们就要稍微改一下我们的开始滑动时回调这个方法了:onNestedScroll()

    @Override
    // 隐藏动画是否正在执行
    private boolean isAnimatingOut = false;
    
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child,
    View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        if ((dyConsumed > 0 || dyUnconsumed > 0) && !isAnimatingOut
        && child.getVisibility() == View.VISIBLE) {// 手指上滑,隐藏FAB
            AnimatorUtil.scaleHide(child, listener);
        } else if ((dyConsumed < 0 || dyUnconsumed < 0) && child.getVisibility() != View.VISIBLE) {
            AnimatorUtil.scaleShow(child, null);// 手指下滑,显示FAB
        }
    }
    
    private ViewPropertyAnimatorListener listener = new ViewPropertyAnimatorListener() {
        @Override
        public void onAnimationStart(View view) {
            isAnimatingOut = true;
        }
    
        @Override
        public void onAnimationEnd(View view) {
            isAnimatingOut = false;
            view.setVisibility(View.GONE);
        }
    
        @Override
        public void onAnimationCancel(View arg0) {
            isAnimatingOut = false;
        }
    };
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    好吧,代码非常少,完成了。那么我们就在string.xml中定义好,刚才我们引用的变量@string/scale_down_show_behavior

    <string name="scale_down_show_behavior">com.yanzhenjie.definebehavior.behavior.ScaleDownShowBehavior</string>
    • 1

    啊呀,好激动呀,我赶紧运行一下。但是但是。。。运行后发现见鬼啊,只有FAB会跟着显示和隐藏,完全看不到Tab导航呀,严振杰你是在忽悠人麽?哈哈哈哈,且听我细细道来。

    通过监听ScaleDownShowBehavior中的view显示/隐藏来控制Tab导航栏

    其实只要看过Material Design系列,Behavior之BottomSheetBehavior与BottomSheetDialog这篇文章的同学会发现,用BottomSheetBehavior的控件默认都是隐藏起来的,需要我们去调用它的方法来控制它的View的显示。所以我们这里需要在CoordinatorLayout中的ContentView滚动的时候来调用BottomSheetBehavior的方法使它依附的View显示与隐藏。

    那么我们发现ScaleDownShowBehavior被系统自动调用了,也触发了View的隐藏和显示,CoordinatorLayout这货没有给我们自动调用BottomSheetBehavior,我们怎么办?如果你没有忘记的话,我们自定义ScaleDownShowBehavior的时候,在onNestedScroll()方法中有个地方是去调用了FAB的显示和隐藏,所以我们在这里加一个回调监听,让外部可以监听到它的动作,是不是同时可以控制BottomSheetBehavior了?如果还没有向明的话看代码。

    先在ScaleDownShowBehavior中定一个Listener

    // 外部监听显示和隐藏。
    public interface OnStateChangedListener {
        void onChanged(boolean isShow);
    }
    • 1
    • 2
    • 3
    • 4

    然后在ScaleDownShowBehavioronNestedScroll()方法中回调:

    private OnStateChangedListener mOnStateChangedListener;
    
    public void setOnStateChangedListener(OnStateChangedListener mOnStateChangedListener) {
        this.mOnStateChangedListener = mOnStateChangedListener;
    }
    
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child,
    View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        if ((dyConsumed > 0 || dyUnconsumed > 0) && !isAnimatingOut
        && child.getVisibility() == View.VISIBLE) {//往下滑
            AnimatorUtil.scaleHide(child, viewPropertyAnimatorListener);
            if (mOnStateChangedListener != null) {
                mOnStateChangedListener.onChanged(false);
            }
        } else if ((dyConsumed < 0 || dyUnconsumed < 0) && child.getVisibility() != View.VISIBLE) {
            AnimatorUtil.scaleShow(child, null);
            if (mOnStateChangedListener != null) {
                mOnStateChangedListener.onChanged(true);
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    好完美啊。来来来,设置一个监听。。。我勒个去,突然发现怎么从FAB拿到这个Behavior啊?且看我下面的分析,保证让你柳暗花明又一村啊。

    拿到FAB的Behavior对象,通过监听控制BottomSheetBehavior的View的显示/隐藏

    我们这知道,给一个View设置Behavior对象的时候是在xml中设置,所以Behavior是一个ViewLayoutParams属性吧?哈哈哈明白了吧,然后Behavior又必须和CoordinatorLayout结合使用,不然也是扯淡,so,这个View也必须是CoordinatorLayout的子View,所以在ScaleDownShowBehavior中产生了如下的一个静态方法(为了方便阅读,提示写了中文):

    public static <V extends View> ScaleDownShowBehavior from(V view) {
        ViewGroup.LayoutParams params = view.getLayoutParams();
        if (!(params instanceof CoordinatorLayout.LayoutParams)) {
            throw new IllegalArgumentException("这个View不是CoordinatorLayout的子View");
        }
        CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior();
        if (!(behavior instanceof ScaleDownShowBehavior)) {
            throw new IllegalArgumentException("这个View的Behaviro不是ScaleDownShowBehavior");
        }
        return (ScaleDownShowBehavior) behavior;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    所以我们在Activity中:

    private BottomSheetBehavior mBottomSheetBehavior;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.zhihu_main);
    
        ScaleDownShowBehavior scaleDownShowFab = ScaleDownShowBehavior.from(FAB);
        scaleDownShowFab.setOnStateChangedListener(onStateChangedListener);
        mBottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.tab_layout));
    }
    
    private OnStateChangedListener onStateChangedListener = new OnStateChangedListener() {
        @Override
        public void onChanged(boolean isShow) {
            mBottomSheetBehavior.setState(
            isShow ? BottomSheetBehavior.STATE_EXPANDED
            : BottomSheetBehavior.STATE_COLLAPSED);
        }
    };
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    哎哟喂,不知不觉中已经把我们的效果实现了,这里最重要的就是onStateChangedListener了,这里实现了Tab导航的隐藏和显示,它的状态是从ScaleDownShowBehavior中回调出来的。

    页面初始化好后显示Tab导航

    我们上文中说道,添加了BottomSheetBehavior属性的View,默认是隐藏的,所以我们在页面初始化时要把我们的Tab导航显示出来:

    private boolean initialize = false;
    
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (!initialize) {
            initialize = true;
            mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Ok,今天的博客就撸完了,本例源码传送门


    版权声明:转载必须注明本文转自严振杰的博客: http://blog.csdn.net/yanzhenjie1003

    登录后您可以享受以下权益:

    ×
    评论 33
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包

    打赏作者

    布莱克‌

    你的鼓励将是我创作的最大动力

    ¥1 ¥2 ¥4 ¥6 ¥10 ¥20
    扫码支付:¥1
    获取中
    扫码支付

    您的余额不足,请更换扫码支付或充值

    打赏作者

    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值

    举报

    选择你想要举报的内容(必选)
    • 内容涉黄
    • 政治相关
    • 内容抄袭
    • 涉嫌广告
    • 内容侵权
    • 侮辱谩骂
    • 样式问题
    • 其他
    点击体验
    DeepSeekR1满血版
    程序员都在用的中文IT技术交流社区

    程序员都在用的中文IT技术交流社区

    专业的中文 IT 技术社区,与千万技术人共成长

    专业的中文 IT 技术社区,与千万技术人共成长

    关注【CSDN】视频号,行业资讯、技术分享精彩不断,直播好礼送不停!

    关注【CSDN】视频号,行业资讯、技术分享精彩不断,直播好礼送不停!

    客服 返回顶部