Android开发项目实战:实现折叠式布局

android:layout_width=“match_parent”

android:layout_height=“match_parent”

app:layout_behavior=“@string/appbar_scrolling_view_behavior”>

<LinearLayout

android:layout_width=“match_parent”

android:layout_height=“match_parent”

android:orientation=“vertical”>

<TextView

android:layout_width=“match_parent”

android:layout_height=“50dp”

android:text=“hello world” />

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

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

image

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

<activity

android: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)

@Override

protected 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() {

@Override

public void onClick(View v) {

onBackPressed();

}

});

}

}

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

Caused by: java.lang.IllegalStateExcepti外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

资料获取→专栏
on: 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"不配置,效果图:

image

即使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;

@Override

public 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() {

@Override

public 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 {

@Override

public 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 {

@Override

public 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) {

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) {

  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现按钮控制布局折叠,可以使用 Android 中的 `ConstraintLayout` 和 `ConstraintSet`。 首先,在 XML 布局文件中,将需要折叠布局放在一个 `ConstraintLayout` 中,并添加一个按钮,用于控制折叠。例如: ```xml <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/collapsedLayout" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/someView"> <!-- 折叠布局 --> <TextView android:id="@+id/collapsedTextView" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorAccent" android:text="Collapsed Text" android:textColor="@android:color/white" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> <!-- 控制折叠的按钮 --> <Button android:id="@+id/expandButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Expand" app:layout_constraintTop_toBottomOf="@id/collapsedTextView" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> </androidx.constraintlayout.widget.ConstraintLayout> ``` 接下来,在 Java/Kotlin 代码中,将需要折叠布局和按钮分别用 `findViewById` 获取,并创建一个 `ConstraintSet` 对象,用于设置折叠和展开后的布局约束。例如: ```kotlin val collapsedLayout = findViewById<ConstraintLayout>(R.id.collapsedLayout) val collapsedTextView = findViewById<TextView>(R.id.collapsedTextView) val expandButton = findViewById<Button>(R.id.expandButton) // 创建 ConstraintSet 对象 val constraintSet = ConstraintSet() // 设置折叠后的布局约束 constraintSet.clone(collapsedLayout) constraintSet.constrainHeight(R.id.collapsedTextView, 0) // 设置展开后的布局约束 val expandedHeight = resources.getDimensionPixelSize(R.dimen.expanded_height) constraintSet.constrainHeight(R.id.collapsedTextView, expandedHeight) // 点击按钮控制折叠和展开 expandButton.setOnClickListener { if (collapsedTextView.height == 0) { // 折叠状态,展开 constraintSet.applyTo(collapsedLayout) expandButton.text = "Collapse" } else { // 展开状态,折叠 constraintSet.applyTo(collapsedLayout) expandButton.text = "Expand" } } ``` 这样,点击按钮时即可控制布局折叠和展开。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值