Material Design系列 - 自定义Behavior实现伸缩标题栏

简介: 引言CoordinatorLayout+CollapsingToolbarLayout+Behavior真是一个好东西,很多复杂的UI交互效果都可以通过Behavior来实现,用了Behavior之后腰也不疼了,再也不会对设计师说这个实现不了了,只要给我时间我就实现给你看!今天带来第一个自定义Behavior:实现一个伸缩的标题栏。

引言

CoordinatorLayout+CollapsingToolbarLayout+Behavior真是一个好东西,很多复杂的UI交互效果都可以通过Behavior来实现,用了Behavior之后腰也不疼了,再也不会对设计师说这个实现不了了,只要给我时间我就实现给你看!今天带来第一个自定义Behavior:实现一个伸缩的标题栏。

效果图如下

img_b337fbc56ee53b83b08fdb2d126314af.gif
Behavior效果图

实现思路

  1. 监听CollapsingToolbarLayout滚动的Y轴距离,和CollapsingToolbarLayout的总高度进行百分比计算得出当前滑动的百分比,再不断的计算顶部图标的宽高进行百分比缩减。整个按钮的X轴坐标跟随百分比减少。
  2. 整个View的宽度除以4,得出每个menu所占的宽度,用item的的下标乘以menu的宽度得出每个menu的X轴。
  3. 当滑动的时候改变文字的透明度,大于0.4则隐藏文字。

开始编码

引入相关依赖

dependencies{
        implementation 'com.android.support:design:26.0.2'
}

创建相关View

创建xml,CollapsingToolbarLayout定义高度和滚动模式,内部放一个View作为滑动的坐标参考。而menu是一个垂直的LinearLayout,上面一个ImageView,下面一个TextView,下面是相关内容:

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".AlipayBehaviorActivity">
    <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:elevation="2dp">

            <android.support.design.widget.CollapsingToolbarLayout
                android:layout_width="match_parent"
                android:layout_height="135dp"
                android:background="@color/colorPrimary"
                app:layout_scrollFlags="scroll|exitUntilCollapsed">
                <!--滚动模式-->
                
                <!--用来做背景坐标参考-->
                <FrameLayout
                    android:id="@+id/flScroll"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    app:layout_collapseMode="parallax"
                    app:layout_collapseParallaxMultiplier="0.9" />

                <android.support.v7.widget.Toolbar
                    android:layout_width="match_parent"
                    android:layout_height="48dp"
                    android:background="@color/colorPrimary"
                    app:layout_anchor="@id/flScroll"
                    app:layout_collapseMode="pin"
                    app:title="" />
                    <!--Toolbar的layout_collapseMode设置为pin,代表toolbar一直固定在顶部-->

            </android.support.design.widget.CollapsingToolbarLayout>
        </android.support.design.widget.AppBarLayout>

        <!--menu布局-->
        <LinearLayout
            style="@style/ServerShortcutMenuLineStyle"
            android:gravity="center">

            <ImageView
                style="@style/ServerShortcutMenuImageStyle"
                android:src="@mipmap/icon_server_app_door" />

            <TextView
                style="@style/ServerShortcutMenuTextStyle"
                android:text="门禁" />
        </LinearLayout>
</android.support.design.widget.CoordinatorLayout>

为了完成后方便复制,我将样式抽取出来了,样式文件如下:

 <style name="ServerShortcutMenuLineStyle">
        <!--快捷菜单行样式-->
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">70dp</item>
        <item name="android:background">?selectableItemBackground</item>
        <item name="android:clickable">true</item>
        <item name="android:focusable">true</item>
        <item name="android:gravity">center_horizontal|bottom</item>
        <item name="android:orientation">vertical</item>
        <item name="android:elevation">5dp</item>
    </style>

    <style name="ServerShortcutMenuTextStyle">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_marginTop">4dp</item>
        <item name="android:textColor">#fff</item>
        <item name="android:textSize">14sp</item>
    </style>

    <style name="ServerShortcutMenuImageStyle">
        <item name="android:layout_width">30dp</item>
        <item name="android:layout_height">30dp</item>
    </style>

先运行起来看看效果吧:

img_0609729d78b51c8f18714eef48d6ae84.png
View1

确定第一个menu的位置

创建AlipayBehavior继承至CoordinatorLayout.Behavior<LinearLayout>,按照思路,先确定第0个item的X和Y轴。核心代码如下:

@Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, LinearLayout child, View dependency) {
        //计算出每个View的宽度
        mViewWidth = dependency.getWidth() / 4/*四个View*/;
        //高度
        mViewHeight = child.getHeight();
        //重新规划 宽度
        ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
        if (layoutParams != null) {
            layoutParams.width = mViewWidth;
        }
        child.setLayoutParams(layoutParams);

        //设置X轴坐标
        child.setX(0);

        // 设置Y轴坐标
        child.setY(mViewHeight/2);

        return true;
    }

在xml中进行引用:

<LinearLayout
        style="@style/ServerShortcutMenuLineStyle"
        android:gravity="center"
        app:layout_anchor="@id/flScroll"
        app:layout_behavior="android.of.road.com.behavior.AlipayBehavior">

        <ImageView
            style="@style/ServerShortcutMenuImageStyle"
            android:src="@mipmap/icon_server_app_door" />

        <TextView
            style="@style/ServerShortcutMenuTextStyle"
            android:text="门禁" />
    </LinearLayout>

运行起来看看效果吧:

img_7de77514a383d118e5523c2a38f9942e.png
View2

可以看到,第0个View的距离已经确定下来,下一步就需要开始跟随滑动而更改menu的位置了。

监听滑动,更改menu的X轴位置

  • 滑动的百分比为dependency的Y轴位置除以dependency的高度。
  • 为了能让menu滑动到最小后又能滑动到最大位置,需要用两个变量mViewMaxX和mViewMaxY存储最大值。
  • 跟随监听更改menu的(mViewMaxX和mViewMaxY)乘以百分比的X轴和Y轴坐标。

代码实现如下:

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, LinearLayout child, View dependency) {
    //计算出每个View的宽度
    mViewWidth = dependency.getWidth() / 4/*四个View*/;
    //高度
    mViewHeight = child.getHeight();

    //计算居中X轴,第一个  随便给的默认值
    mViewMaxX = 50;
    //计算Y轴 坐标系参考View的高度二分之一减去menu的高度除以2,刚好居中
    mViewMaxY = dependency.getHeight() / 2 - mViewHeight / 2;
    //重新规划 宽度
    ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
    if (layoutParams != null) {
        layoutParams.width = mViewWidth;
    }
    child.setLayoutParams(layoutParams);

    //计算百分比  当前的百分比其实是没有减去状态栏的
    float mPercent = dependency.getY() / (dependency.getHeight());
    if (mPercent >= 1f)
        mPercent = 1;

    //更改 内部文字的透明底
    View mTextTitleView = child.getChildAt(1);
    if (mTextTitleView != null) {
        mTextTitleView.setAlpha(1 - (mPercent > 0.4 ? 1 : mPercent));
    }

    // 更改内部imageView的大小
    View mImageTitleView = child.getChildAt(0);
    if (mImageTitleView != null) {
        mImageTitleView.setScaleX(1 - (0.4f * mPercent));
        mImageTitleView.setScaleY(1 - (0.4f * mPercent));
    }

    //设置X轴坐标
    child.setX(mViewMaxX - mViewMaxX * mPercent);
    // 设置Y轴坐标
    child.setY(mViewMaxY - (mViewMaxY * 1.4f) * mPercent);

    return true;
}

效果:

img_67215c538925f3d58e46efefe077d1d3.gif
初步完成监听

分配到每个menu上

现在已经完成了,现在需要的是为每个menu进行配置Behavior,实现思路如下:

  • 这里的实现思路是为每个menu加一个tag,而tag就是menu的下标
  • Behavior中获取menu的下标,根据下标来确定x和y轴的位置

xml代码如下:

 <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:elevation="2dp">

        <android.support.design.widget.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="135dp"
            android:background="@color/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <!--用来做背景坐标参考-->
            <FrameLayout
                android:id="@+id/flScroll"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.9" />


            <android.support.v7.widget.Toolbar
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:background="@color/colorPrimary"
                app:layout_anchor="@id/flScroll"
                app:layout_collapseMode="pin"
                app:title="" />

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/layout_apay_content" />

    <LinearLayout
        style="@style/ServerShortcutMenuLineStyle"
        android:gravity="center"
        android:tag="0"
        app:layout_anchor="@id/flScroll"
        app:layout_behavior="android.of.road.com.behavior.AlipayBehavior">

        <ImageView
            style="@style/ServerShortcutMenuImageStyle"
            android:src="@mipmap/icon_server_app_door" />

        <TextView
            style="@style/ServerShortcutMenuTextStyle"
            android:text="门禁" />
    </LinearLayout>

    <LinearLayout
        style="@style/ServerShortcutMenuLineStyle"
        android:gravity="center"
        android:tag="1"
        app:layout_anchor="@id/flScroll"
        app:layout_behavior="android.of.road.com.behavior.AlipayBehavior">

        <ImageView
            style="@style/ServerShortcutMenuImageStyle"
            android:src="@mipmap/icon_server_app_scanf" />

        <TextView
            style="@style/ServerShortcutMenuTextStyle"
            android:text="扫一扫" />
    </LinearLayout>

    <LinearLayout
        style="@style/ServerShortcutMenuLineStyle"
        android:gravity="center"
        android:tag="2"
        app:layout_anchor="@id/flScroll"
        app:layout_behavior="android.of.road.com.behavior.AlipayBehavior">

        <ImageView
            style="@style/ServerShortcutMenuImageStyle"
            android:src="@mipmap/icon_server_app_card" />

        <TextView
            style="@style/ServerShortcutMenuTextStyle"
            android:text="停车月卡" />
    </LinearLayout>

    <LinearLayout
        style="@style/ServerShortcutMenuLineStyle"
        android:gravity="center"
        android:tag="3"
        app:layout_anchor="@id/flScroll"
        app:layout_behavior="android.of.road.com.behavior.AlipayBehavior">

        <ImageView
            style="@style/ServerShortcutMenuImageStyle"
            android:src="@mipmap/icon_server_app_wallet" />

        <TextView
            style="@style/ServerShortcutMenuTextStyle"
            android:text="钱包" />
    </LinearLayout>
</android.support.design.widget.CoordinatorLayout>

完整Java代码如下:

public class AlipayBehavior extends CoordinatorLayout.Behavior<LinearLayout> {

    /**
     * 下标
     */
    private int mPosition = -1;
    /**
     * X轴坐标
     */
    private float mViewMaxX = 0;

    /**
     * View的宽度
     */
    private int mViewWidth;
    /**
     * View的高度
     */
    private int mViewHeight;

    /**
     * Y轴的最大高度
     */
    private int mViewMaxY = 0;


    public AlipayBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, LinearLayout child, View dependency) {
        return dependency instanceof Toolbar;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, LinearLayout child, View dependency) {
        if (mPosition == -1) {//未初始化
            mPosition = Integer.parseInt((String) child.getTag());
            //计算出每个View的宽度
            mViewWidth = dependency.getWidth() / 4/*四个View*/;
            //高度
            mViewHeight = child.getHeight();
            //重新规划 宽度
            ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
            if (layoutParams != null) {
                layoutParams.width = mViewWidth;
            }
            child.setLayoutParams(layoutParams);

            // 总宽度 除以四。得出每一个子View的宽度,居中为
            //计算居中X轴
            mViewMaxX = mViewWidth * mPosition;
            //计算Y轴
            mViewMaxY = (int) (child.getY() + DensityUtils.dp2px(parent.getContext(), 50f));
        }

        //计算百分比  当前的百分比其实是没有减去状态栏的
        float mPercent = dependency.getY() / (dependency.getHeight()/* - ScreenUtils.getStatusHeight(parent.getContext())*/);
        if (mPercent >= 1f)
            mPercent = 1;

        // 动态更改 View的高度
        ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
        if (layoutParams != null) {
            layoutParams.height = (int) (mViewHeight - (mViewHeight /** 0.8f*/) * mPercent);
            layoutParams.width = (int) (mViewWidth - (mViewWidth * mPercent));
            child.setLayoutParams(layoutParams);
        }

        //更改 内部文字的透明底
        View mTextTitleView = child.getChildAt(1);
        if (mTextTitleView != null) {
            mTextTitleView.setAlpha(1 - (mPercent > 0.4 ? 1 : mPercent));
        }

        // 更改内部imageView的大小
        View mImageTitleView = child.getChildAt(0);
        if (mImageTitleView != null) {
            mImageTitleView.setScaleX(1 - (0.4f * mPercent));
            mImageTitleView.setScaleY(1 - (0.4f * mPercent));
        }


        //设置X轴坐标//没有计算状态栏的情况之下,滑动并不是完整的
        child.setX(mViewMaxX - (mViewMaxX - 50/*左边的距离*/) * mPercent);


        // 设置Y轴坐标
        child.setY(mViewMaxY - (mViewMaxY * 1.4f) * mPercent);


        return true;
    }
}

查看效果图:

img_126aac5b1d7b25cc5955550d1f134c89.gif
完成监听

最后

未完待续、敬请期待!

img_1ee92a858822d3b1d90a45e40e7b1042.jpe
FullScreenDeveloper

源码地址

目录
相关文章
|
5天前
|
存储 弹性计算 缓存
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
本文更新了2026年阿里云全系列云服务器租赁活动报价,所有特惠资源均可前往阿里云活动中心选购,整体覆盖从个人入门到企业级高性能场景的全梯度需求。其中轻量应用服务器主打极致性价比,2核2G峰值200M带宽配置每日10点、15点限时抢购价仅38元/年,2核4G配置379元/年起;高性价比的经济型e实例、通用算力型u2i实例覆盖2核4G至4核32G全档位,适配开发测试与中小型企业业务;搭载英特尔至强6处理器的第九代c9i企业级实例算力较上代提升20%,支撑高并发生产环境,不同实例规格价差清晰,用户可根据自身业务负载与预算灵活选型。
1573 112
|
12天前
|
云安全 人工智能 运维
阿里云联动百位企业安全专家,共识Agent防御最佳实践
当Agent成为新员工,你的安全边界在哪里?
1939 8
阿里云联动百位企业安全专家,共识Agent防御最佳实践
|
6天前
|
人工智能 程序员 API
Codex 接入 DeepSeek-V4-Flash:还能补上识图,提供两套方案
Codex 接入 DeepSeek-V4-Flash 怎么配?本文覆盖 CLI 与桌面端,再用 qwen3-vl-flash 补识图,两套方案可直接照做
|
6天前
|
编解码 人工智能 安全
2核4G/4核8G/8核16G阿里云服务器如何选择实例?经济型e、通用算力型u2i与计算型c9i选哪个?
本文介绍了阿里云2核4G、4核8G、8核16G三档主流配置下经济型e、通用算力型u2i和计算型c9i三种实例的最新活动价格与适用场景。同配置下三者价差显著,以2核4G为例,经济型e低至599.93元/年,计算型c9i则高达1742.08元/年。文章详细解析了各实例的性能定位:经济型e适合轻负载入门场景,u2i兼顾稳定算力与性价比,c9i凭借第9代至强处理器与芯片级安全能力支撑高性能业务。同时提示用户可叠加满减优惠券享受折上折,建议根据业务负载与预算综合决策。
526 112
|
18天前
|
人工智能 前端开发 Linux
Codex 桌面版安装 + CC Switch 接入第三方 API 完整教程(2026 最新)
2026最新教程:手把手教你安装Codex桌面版,通过CC Switch v3.17.0一键接入Fenno等国产API(兼容OpenAI Responses格式),跳过账号登录,完整启用代码审查、多步任务与上下文感知功能。零基础友好,全程图文实操。(239字)
2565 4
|
10天前
|
存储 人工智能 关系型数据库
阿里云AI产品与云产品最新组合套餐:Token Plan、AI coding及云服务器和建站等组合优惠价
阿里云推出全新“算力+模型+应用”一站式云与AI组合套餐活动,覆盖从个人开发者到中大型企业的全场景需求。核心亮点为分三档定价的Token Plan订阅服务,支持Qwen3.8-Max-Preview大模型调用,错峰时段最低可享0.2折优惠。活动同步推出AI Coding、智能体部署、云电脑托管、0代码建站等十余类场景化组合,搭配99元/年的普惠云服务器、88元/年的入门数据库等经典特惠产品,还为企业提供1V1定制化AI转型方案,大幅降低了不同用户群体拥抱AI的技术门槛与采购成本。
721 111
|
20天前
|
人工智能 JSON 安全
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
阿里云AI安全产品联动防御Fastjson攻击
2634 13
Fastjson远程代码执行漏洞,阿里云AI安全为您保驾护航
|
6天前
|
人工智能 JSON Shell
2026AI漫剧本地全开源方案(附各个软件模型链接),8G显卡也能流畅运行
这是一套完全本地化部署的AI漫剧生成技术链路:涵盖LLM剧本分镜生成、FLUX文生图(IP-Adapter人脸锁定)、StoryDiffusion时序连贯控制、LTX-2.3唇形同步视频生成,及ComfyUI全流程调度。零云端费用,仅耗硬件算力,单集2–4小时可产出竖屏短视频,适配抖音/B站分发。
|
7天前
Qoder 一周年 × Qwen3.8-Max 正式上线,多重好礼限时领
8月3日,Qwen3.8-Max 正式上线Qoder,迎来Qoder一周年。新老用户可领800次免费调用,下单再赠2000次;夜间(22:00–08:00)调用5折;邀请好友双方得积分与调用额度。
447 1

热门文章

最新文章