本文出自门心叼龙的博客,属于原创类容,转载请注明出处。

在上一篇文章我们实现了一个头部固定的ExpandedListView,今天需要在它的基础上实现:在它头部加一个背景图片,默认状态下他处于展开状态,往上滑的时候背景图片逐渐的折叠起来,往下滑的时候背景图片慢慢的展开效果图如下:

通过CoordinateLayout实现的折叠式布局

有人可能会说这不就是折叠式布局吗?是的,这就是Android 5.0给我们提供的材料设计库中的CoordinateLayout就是解决这个问题的,使用CoordinateLayout来协调ScrollView,NestedScrollView,ListView,RecycleView和顶部的背景图片、ToolBar之间的滚动关系、在很多的手机应用中,时不时会看到关于折叠布局的效果,现在我们先看看CoordinateLayout是怎么实现的然后在讲我们自定义实现一个折叠式布局,直接上代码:

<?xml version="1.0" encoding="utf-8"?>
<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.AppBarLayoutandroid:layout_width="match_parent"android:layout_height="200dp"><android.support.design.widget.CollapsingToolbarLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"app:layout_scrollFlags="scroll|exitUntilCollapsed"app:titleEnabled="false"><ImageViewandroid:layout_width="match_parent"android:layout_height="match_parent"android:scaleType="centerCrop"android:src="@mipmap/homepage_pic_banner"app:layout_collapseMode="parallax" /><android.support.v7.widget.Toolbarandroid:id="@+id/view_toolbar"android:layout_width="match_parent"android:layout_height="?attr/actionBarSize"app:layout_collapseMode="pin"app:popupTheme="@style/ThemeOverlay.AppCompat.Light"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:text="新闻详情" /></android.support.v7.widget.Toolbar></android.support.design.widget.CollapsingToolbarLayout></android.support.design.widget.AppBarLayout><android.support.v4.widget.NestedScrollViewandroid:layout_width="match_parent"android:layout_height="match_parent"app:layout_behavior="@string/appbar_scrolling_view_behavior"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewandroid:layout_width="match_parent"android:layout_height="50dp"android:text="hello world" />......</LinearLayout></android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>


以上就是实现一个折叠式布局的典型模板布局代码,一个简简单单的布局就实现了这样的效果,但是必须要注意在AndroidMnifest.xml必须要给Activity指定它的theme为NoActionBar的样式代码如下:

 <activityandroid:name=".test.CoordinatorLayoutTestActivity"android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>

否则会出现ActionBar和ToolBar共存的情况,的显示效果如下:

另外还需要把自己自定义的ToolBar告诉给系统,即第9行的setSupportActionBar(toolbar),否则我们的ToolBar会作为一个普通的View而存在

public class CoordinatorLayoutTestActivity extends AppCompatActivity {@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_coordinator_layout_test);Toolbar toolbar = findViewById(R.id.view_toolbar);setSupportActionBar(toolbar);getSupportActionBar().setDisplayShowTitleEnabled(false);toolbar.setNavigationIcon(R.mipmap.callback_white_icon);toolbar.setNavigationOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {onBackPressed();}});}
}

如果只指定了 setSupportActionBar(toolbar),没有AndroidMnifest.xml在指定Activity的theme为NoActionBar,那就运行就直接崩溃了,会报错如下:

 Caused by: java.lang.IllegalStateException: This Activity already has an action bar supplied by the window decor. Do not request Window.FEATURE_SUPPORT_ACTION_BAR and set windowActionBar to false in your theme to use a Toolbar instead.at android.support.v7.app.AppCompatDelegateImpl.setSupportActionBar(AppCompatDelegateImpl.java:345)at android.support.v7.app.AppCompatActivity.setSupportActionBar(AppCompatActivity.java:130)

意思是说Activity已经有一个ActionBar了,请在你的样式中使用ToolBar替代

在上面的布局文件代码中,根布局CoordinatorLayout 就是用来协调AppBarLayout和NestedScrollView之间滚动的,40行的NestedScrollView是我们要滚动的内容,在11行的CollapsingToolbarLayout标签的内部就是要折叠的内容

  • 其中43行的 app:layout_behavior不配置的效果:

    NestedScrollView的内容在ToolBar之上滚动

  • 其中13行app:layout_scrollFlags="scroll|exitUntilCollapsed"如果不配置效果图如下:

    如果没有配置则CollapsingToolbarLayout包裹内容内容就会固定在顶部,不会滚动

  • 28行 app:layout_collapseMode="pin"不配置,效果图:

    ToolBar会跟随NestedScrollView的滚动而滚动,而不会固定在布局顶部位置

  • 14行app:titleEnabled="false"不配置,效果图:

    即使33行的TextView配置了android:layout_gravity=“center”,title也不会居中显示

我们感觉折叠式布局就是给我们的View设置相关的属性配置,不需要进行任何编码就能完成我们的折叠效果,我们不得的赞叹android 5.0给我们提供这一强大的功能

我们来总结一下:
CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout结合起来才能产生这么神奇的效果,不要幻想使用其中的一个控件就能完成这样的效果

ToolBar的设置

系统默认使用的就是系统自带的ActionBar,如果我们要使用自定义的ToolBar,就必须明确的告诉Activity不需要使用系统自带的ActionBar即要给activity设置NoActionBar的样式,另外必须调用setSupportActionBar(toolbar)将自己定义的ToolBar设置给Activity。

CoordinatorLayout下可滑动控件的设置

CoordinatorLayout作为整个布局的父布局容器。给你的可以滑动的控件例如RecyclerView设置如下属性:app:layout_behavior=@string/appbar_scrolling_view_behavior
CoordinatorLayout还提供了layout_anchor 和 layout_anchorGravity属性一起配合使用,可以用于设置FloatingActionButton的位置,此处我是放在appBar的右下角。
app:layout_anchor="@id/appbar"
app:layout_anchorGravity=“bottom|right|end”

CollapsingToolbarLayout的layout_scrollFlags属性

AppBarLayout里面定义的子view只要设置了app:layout_scrollFlags属性,就可以在RecyclerView滚动事件发生的时候被触发某种行为
例如我给CollapsingToolbarLayout控件设置了 app:layout_scrollFlags="scroll|exitUntilCollapsed"此刻如果没有这个属性,CollapsingToolbarLayout是不会折叠的那么问题来了,layout_scrollFlags中的属性值除了可以触发折叠的行为,还有其它的属性值吗?并且各个属性的意义是什么?scroll至少有一个scroll,即可滚动。

属性 作用
scroll 必须要给其至少有设置一个scroll,即可滚动
enterAlways 向下滚动即可见。例如下拉时,立即显示Toolbar
exitUntilCollapsed 这个flag是定义何时收缩。当你定义了一个minHeight,这个view将在滚动到达这个最小高度的时候消失
enterAlwaysCollapsed 这个flag是定义何时展开。当你定义了一个最小高度minHeight, 同时enterAlways也定义了,那么view将在到达这个最小高度的时候开始展示
snap 当一个滚动事件结束,它将根据显示百分比的大小自动滚动到收缩或展开。

如果不设置该属性,则该布局不能滑动

CollapsingToolbarLayout的其他属性

另外还可以给CollapsingToolbarLayout设置以下属性:

属性 作用
contentScrim 设置当完全折叠(收缩)后的背景颜色。
expandedTitleMarginEnd 没有扩张的时候标题显示的位置
expandedTitleMarginStart 扩张的时候标题向左填充的距离。
statusBarScrim 设置折叠时状态栏的颜色

CollapsingToolbarLayout下的view的layout_collapseMode属性

CollapsingToolbarLayout里面定义的view只要设置了app:layout_collapseMode属性,就可以控制子视图的折叠模式。
折叠模式分为两种:

属性 作用
pin 固定模式。在收缩的时候最后固定在顶端(例如向上滚动的时候就固定toolBar)
parallax 视差模式,在折叠的时候会有个视差折叠的效果。(例如向下滚动的时候就展开ImageView)

CoordinatorLayout 的fitsSystemWindows属性

fitsSystemWindows属性可以让view根据系统窗口来调整自己的布局,简单点说就是我们在设置应用布局时是否考虑系统窗口布局,这里系统窗口包括系统状态栏、导航栏、输入法等,包括一些手机系统带有的底部虚拟按键。android:fitsSystemWindows=”true” (触发View的padding属性来给系统窗口留出空间) 这个属性可以给任何view设置,只要设置了这个属性此view的其他所有padding属性失效,同时该属性的生效条件是只有在设置了透明状态栏(StatusBar)或者导航栏(NavigationBar)此属性才会生效

如何监听CollapsingToolbarLayout的展开与折叠

使用官方提供的 AppBarLayout.OnOffsetChangedListener就能实现了,不过要封装一下才好用,自定义一个继承了 AppBarLayout.OnOffsetChangedListener的类这里命名为AppBarStateChangeListener

public abstract class AppBarStateChangeListener implements AppBarLayout.OnOffsetChangedListener {public enum State {EXPANDED,COLLAPSED,IDLE}private State mCurrentState = State.IDLE;@Overridepublic final void onOffsetChanged(AppBarLayout appBarLayout, int i) {if (i == 0) {if (mCurrentState != State.EXPANDED) {onStateChanged(appBarLayout, State.EXPANDED);}mCurrentState = State.EXPANDED;} else if (Math.abs(i) >= appBarLayout.getTotalScrollRange()) {if (mCurrentState != State.COLLAPSED) {onStateChanged(appBarLayout, State.COLLAPSED);}mCurrentState = State.COLLAPSED;} else {if (mCurrentState != State.IDLE) {onStateChanged(appBarLayout, State.IDLE);}mCurrentState = State.IDLE;}}public abstract void onStateChanged(AppBarLayout appBarLayout, State state);
}

然后我们这样使用它:

 mAppBarLayout.addOnOffsetChangedListener(new AppBarStateChangeListener() {@Overridepublic void onStateChanged(AppBarLayout appBarLayout, State state) {Log.d("STATE", state.name());if( state == State.EXPANDED ) {//展开状态                    }else if(state == State.COLLAPSED){//折叠状态                     }else {                //中间状态                }}});

这样就可以在不同的状态下根据自己的业务需求去实现相关的逻辑了

StickyLayout自定折叠式布局的实现

好了,上面就是关于通过CoordinateLayout实现的折叠式布局所有的知识点,如果说前面只是开胃菜,现在我们就开始上主菜了,我们能不能自己实现这样一个折叠式的布局,利用上一篇我们所讲的头部固定的ExpandedListView,把它作为具有滑动功能的主View,在它的顶部添加具有背景图片Header,随着ExpandedListView的滑动header实现扩展和收缩的效果,效果如下:

功能分析

其实这个效果图在文章的一开始就展示过了,整个布局分为上下两部分:上分部分为可折叠的Header,下半部分就是我们头部固定的ExpandedListView,他们公共父view就是今天我们要实现的折叠式布局StickyLayout,ExpandedListView是自身所具备滑动功能的,而我们在整个屏幕上,往上滑动的时候如果header处于展开状态则Header慢慢的要折叠起来,往下滑动的时候如果ExpandedListView顶部数据都显示出来的情况下再往下拉的时候Header就慢慢的展开,其他的状态就是我们的ExpandedListView在上下滑动,也就是说我们的Header在折叠和展开的状态下的这些事件被StickyLayout拦截了,其他的事件就交给ExpandedListView进行处理从而实现了他的上下滑动,这就属于典型的滑动冲突问题,简言之就是我们在上下滑动的过程中的有些事件需要被StickyLayout拦截消掉来实现Header的折叠和展开效果,其他的事件就交给ExpandedListView来实现它的滑动效果
现在我们要思考的是哪些情况下被拦截:

  • 左右滑动的不需要处理,只处理上下滑动的事件
  • 在展开的状态下,上滑事件需要拦截
  • ExpandedListView的第0个元素处于可见状态,此时的下滑事件需要拦截

在事件拦截方法中处理滑动冲突

public class StickyLayout extends LinearLayout {...@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {boolean intercept = false;int x = (int) ev.getX();int y = (int) ev.getY();switch (ev.getAction()){case MotionEvent.ACTION_DOWN:mLastInterceptX = x;mLastInterceptY = y;intercept = false;break;case MotionEvent.ACTION_MOVE:int dx = x - mLastInterceptX;int dy = y - mLastInterceptY;if(y <= mCurrHeaderHeight){intercept = false;}else if(Math.abs(dx) > Math.abs(dy)){intercept = false;}else if(mState == mStateExpand && dy <= - mScaledTouchSlop){//上滑intercept = true;}else if(mGiveUpTouchEventListener.giveUpTouchEvent() && dy > mScaledTouchSlop){//下滑intercept = true;}else{intercept = false;}break;case MotionEvent.ACTION_UP:mLastInterceptX = 0;mLastInterceptY = 0;intercept = false;break;}return intercept;}...
}

上面就是关于事件拦截的核心代码,首先我们看17行:y <= mCurrHeaderHeight 如果触摸事件是在Header之上也就不拦截了,再看19行Math.abs(dx) > Math.abs(dy),如果是横向滑动也不是我们所需要的事件也不拦截,否则上就是上下滑动的事件了,在这个状态下状态Header处于展开状态且是上滑那就需要拦截处理,也就是21行:mState == mStateExpand && dy <= - mScaledTouchSlop所处理的逻辑,在看24行:mGiveUpTouchEventListener.giveUpTouchEvent() && dy > mScaledTouchSlop,giveUpTouchEvent方法表示如果ExpandedListView的第一个可见元素是0且dy > mScaledTouchSlop(表示是上滑)此时的事件也是需要拦截的

View滑动距离常量TouchSlop

在21行细心的同学可能会看到这么一句dy <= - mScaledTouchSlop,dy指的是滑动的距离,mScaledTouchSlop到底是什么?其实他是Android系统给我们提供的View滑动最小距离常量TouchSlop,也就是说两个Move事件之间的滑动距离如果小于这个常量就系统不认为他是滑动,因为滑动距离太短,反之就认为它是滑动,这个常量值和设备有关,不同的设置上这个值可能是不相同的,我们可以通过如下方式即可获取这个常量:

int mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();

折叠展开的事件消费

上面的17到29行就是处理事件拦截的核心处理逻辑,事件拦截完毕,事件就交给TouchEvent方法进行消费了,下面看看Header到底具体是怎么折叠的?其实很简单就是不用重置Header的height就OK了,我们看看代码:

public class StickyLayout extends LinearLayout {...@Overridepublic boolean onTouchEvent(MotionEvent event) {int x = (int) event.getX();int y = (int) event.getY();switch (event.getAction()){case MotionEvent.ACTION_DOWN:break;case MotionEvent.ACTION_MOVE:int dx = x - mLastX;int dy = y - mLastY;mCurrHeaderHeight += dy;setHeaderHeight(mCurrHeaderHeight);break;case MotionEvent.ACTION_UP:int dest = 0;if(mCurrHeaderHeight <= mOriginHeaderHeight * 0.5){dest = 0;mState = mStateCollapsed;}else{dest = mOriginHeaderHeight;mState = mStateExpand;}smoothSetHeaderHeight(mCurrHeaderHeight,dest,500);break;}mLastX = x;mLastY = y;return super.onTouchEvent(event);}...
}

其中12行到13行就是手指拖动状态下的核心逻辑 ,12行计算两次Move事件所移动的距离,13行根据手指滑动的距离来计算Header当前的高度,计算完毕就可以调用setHeaderHeight设置Header的高了

设置Header的高来实现折叠效果

private void setHeaderHeight(int height) {if(height <= 0){height = 0;}else if(height >= mOriginHeaderHeight){height = mOriginHeaderHeight;}if(height == 0 ){mState = mStateCollapsed;}else{mState = mStateExpand;}headerView.getLayoutParams().height = height;headerView.requestLayout();}

其中第2行到第6行对Header高度的越界处理,第7行到11行是设置Header的状态,第12行到13行给Header的高赋值并刷新Header来变它的位置与大小

手指抬起的自动回弹折叠展开效果

如果当前Header的高小于原始高度的一半手指抬起的时候Header进行收缩,反之就进行展开操作,核心代码在上面的onTouchEvent(MotionEvent event)方法的的17行到25行:

  int dest = 0;if(mCurrHeaderHeight <= mOriginHeaderHeight * 0.5){dest = 0;mState = mStateCollapsed;}else{dest = mOriginHeaderHeight;mState = mStateExpand;}smoothSetHeaderHeight(mCurrHeaderHeight,dest,500);

最后在调用smoothSetHeaderHeight实现弹性展开,折叠

    private void smoothSetHeaderHeight(int from,int to,int duration) {ValueAnimator valueAnimator = ValueAnimator.ofInt(from, to).setDuration(duration);valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {setHeaderHeight((Integer) animation.getAnimatedValue());}});valueAnimator.start();}

总结

截止目前整个折叠式自定义View就全部讲完了,事件拦截这块的判断逻辑是整个代码的核心,找到了判断折叠、展开的的算法那么其他的问题也就不是什么大问题了,解决滑动冲突问题也是我们在自定义View开发过程中的常见问题,也是难点问题,只要多练习,多思考就能孰能生巧最后我将整个测试代码传到了github上,欢迎学习下载https://github.com/mxdldev/android-custom-view/,其中StickyLayout.java就是我们本例中的自定义View的全部代码实现,下载完整项目后直接运行安装完毕,点击StickyLayout按钮就进入了我们的测试页面,效果图如下:

问题反馈

在使用学习中有任何问题,请留言,或加入Android、Java开发技术交流群

  • QQ群:810970432
  • email:geduo_83@163.com

Android开发实战手把手教你实现一个折叠式布局相关推荐

  1. Android 开发之手把手教你写 ButterKnife 框架(三)

    系列文章目录导读: Android开发之手把手教你写ButterKnife框架(一) Android开发之手把手教你写ButterKnife框架(二) Android开发之手把手教你写ButterKn ...

  2. android 自定义心电图,手把手教你打造一个心电图效果View Android自定义View

    大家好,看我像不像蘑菇-因为我在学校呆的发霉了. 思而不学则殆 丽丽说得对,我有奇怪的疑问,大都是思而不学造成的,在我书读不够的情况下想太多,大多等于白想 ,所以革命没成功,同志仍需努力. 好了废话不 ...

  3. android 自定义心电图,手把手教你打造一个心电图效果View Android自定义View(示例代码)...

    大家好,看我像不像蘑菇-因为我在学校呆的发霉了. 思而不学则殆 丽丽说得对,我有奇怪的疑问,大都是思而不学造成的,在我书读不够的情况下想太多,大多等于白想 ,所以革命没成功,同志仍需努力. 好了废话不 ...

  4. 实战|手把手教你训练一个基于Keras的多标签图像分类器

    图片来自 pexels,作者:Jean-Christophe André 2019 年第 63 篇文章,总第 87 篇文章 原文链接:https://www.pyimagesearch.com/201 ...

  5. 零基础学习嵌入式入门以及项目实战开发【手把手教+国内独家+原创】

    零基础学习嵌入式入门以及项目实战开发[手把手教+国内独家+原创] 独家拥有,绝对经典                            创 科 之 龙 嵌入式开发经典系列教程 [第一期] 主讲人: ...

  6. Android开发实战---一个汽车销售APP,有汽车列表页、汽车详情页、贷款计算页3个界面。

    Android开发实战设计并实现一个汽车销售APP,要求至少有汽车列表页.汽车详情页.贷款计算页3个界面. 已开源:https://github.com/yan123666/wlf 实现结果: 1.计 ...

  7. PWA入门:手把手教你制作一个PWA应用

    摘要: PWA图文教程 原文:PWA入门:手把手教你制作一个PWA应用 作者:MudOnTire Fundebug经授权转载,版权归原作者所有. 简介 Web前端的同学是否想过学习app开发,以弥补自 ...

  8. 从原理到实现丨手把手教你写一个线程池丨源码分析丨线程池内部组成及优化

    人人都能学会的线程池 手写完整版 1. 线程池的使用场景 2. 线程池的内部组成 3. 线程池优化 [项目实战]从原理到实现丨手把手教你写一个线程池丨源码分析丨线程池内部组成及优化 内容包括:C/C+ ...

  9. 手把手教你写一个中文聊天机器人

    本文来自作者 赵英俊(Enjoy) 在 GitChat 上分享 「手把手教你写一个中文聊天机器人」,「阅读原文」查看交流实录. 「文末高能」 编辑 | 哈比 一.前言 发布这篇 Chat 的初衷是想和 ...

  10. Xamarin Android开发实战(上册)

    Xamarin Android开发实战(上册)大学霸内部资料 试读文档下载地址:http://pan.baidu.com/s/1jGEHhhO 密码:vcfm 介绍: 本教程是国内唯一的Xamarin ...

最新文章

  1. 【点云StatisticalOutlierFilter】python-pcl:去除离群点
  2. Unity3d DLL脚本通用解密方法
  3. Python断言方法:assert
  4. linux 安装软件_Linux:其它软件安装方式
  5. vmware ubuntu重置root密码
  6. Android基站定位——单基站定位(二)
  7. 给MM修电脑的三个步骤
  8. HTTPS科普扫盲帖【转】
  9. zlib、libzip、 libzippp 库编译(windows + cmake + vs2013)
  10. 窄带物联网技术有望终结碎片化现状
  11. afm原子力分析软件_AFM数据处理软件|原子力显微镜配套数据处理软件 nanoscope analysis1.8 官方版 - 极光站...
  12. (半)自动化爬虫系统该包含的功能点及相关介绍
  13. 哪些专业软件可以测试cpu,常用的正经CPU测试软件有哪些
  14. 链家上海2018年9月二手房
  15. [树的直径 树形DP] UOJ #11【UTR #1】ydc的大树
  16. Android 模拟返回键、菜单键、主页键
  17. 面了20家,原来大厂面试的套路是……
  18. Stay hungry stay young
  19. (root) Additional property nginx is not allowed
  20. 2023微软校招开始啦,快来找我内推吧

热门文章

  1. 旅游管理系统项目java设计_基于JSP的旅游管理系统设计与实现(MyEclipse,SQL)
  2. php 视频播放加密,如何在HTML5页面播放加密视频
  3. 从零开始自制实现C++ High-Performance WebServer 全流程记录
  4. python大数据和java大数据的区别-学习大数据先学Python还是JAVA?
  5. linux sigar 进程监控,Linux 下使用Sigar 获取CPU 使用率
  6. 指标公式c语言源码下载,通达信超级厉害指标公式源码附图-通达信公式-公式指标网...
  7. quickpcb添加pcb库_QuickPcb元件库下载
  8. 关于WIN10显示“未安装任何音频输出设备” 英特尔(R)智音系统OED启动错误(代号10)解决办法
  9. jupyter notebook 软件安装及使用
  10. linux系统的课程实践,“Linux操作系统与应用”课程教学与实践