Android中动态初始化布局参数以及ConstraintLayout使用中遇到的坑
Android中动态初始化布局以及ConstraintLayout遇到的一个坑
ConstraintLayout是Android中的一个很强大的布局,它通过控件之间的相对定位,来完成一个layout中的所有view的布局,但布局方法相对于RelativeLayout更为灵活。能够大幅减少布局嵌套,提升性能。
这次遇到的问题是在Activity中动态对Fragment进行布局和动画效果,难点在于Fragment的尺寸是wrap_content的(为了减少在手机屏幕尺寸上的适配成本)。而这个Fragmen在Activity中一开始是隐藏在整个屏幕的下方,在需要的时候才以动画的形式滑上来展现出来,而且一共有三个这样的Fragment,根据状态来选择展现哪一个。类似于歌曲列表那样的功能,正常情况下是隐藏的,只有在点击了按钮之后才会滑上来。
根据需求,选择通过改变Fragment的容器Layout的bottomMargin值来实现初始的布局和动画效果。具体思路如下:
- Fragment容器的布局
<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/fl_frag_container" android:clickable="false" android:longClickable="false" app:layout_constraintBottom_toBottomOf="parent" > <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:layout_marginBottom="0dp" android:id="@+id/fl_welcome_fragment_container">FrameLayout> <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:layout_marginBottom="0dp" android:id="@+id/fl_reg_fragment_container">FrameLayout> <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:layout_marginBottom="0dp" android:id="@+id/fl_main_fragment_container">FrameLayout>FrameLayout>
这部分是Activity布局中和Fragment有关的部分。height是根据Fragment的高确定的,而Fragment又是wrap_content的。因此我们需要在系统完成一次measure以及layout流程之后才能根据Fragment的高去设置这三个容器layout的bottomMargin值,使bottomMargin = -height,以确保它们是隐藏在屏幕下方的。
- Activity中加载Fragment
//在onCreate中调用 private void loadFragments() { binding.flMainFragmentContainer.setVisibility(View.INVISIBLE); binding.flRegFragmentContainer.setVisibility(View.INVISIBLE); binding.flWelcomeFragmentContainer.setVisibility(View.INVISIBLE); mMainFragment = MainFragment.newInstance(mIsLoggedIn, null); mRegistFragment = RegistrationFragment.newInstance(null, null); mWelcomeFragemtn = WelcomeFragment.newInstance(null, null); FragmentManager manager = getSupportFragmentManager(); FragmentTransaction transaction = manager.beginTransaction(); transaction.add(R.id.fl_main_fragment_container, mMainFragment); transaction.add(R.id.fl_reg_fragment_container, mRegistFragment); transaction.add(R.id.fl_welcome_fragment_container, mWelcomeFragemtn); transaction.commit(); }
使用FragmentTransaction来加载Fragment。需要注意的是加载之前我们将那三个容器layout都设置为不可见的。这是因为在加载完Fragment之后,FragmentManager会随着Activity的生命周期将Fragment放在我们指定的layout中并设置layout的参数。接着是测量、布局等。而我们一开始由于不知道高度值,bottomMargin是为0的,如果设置为可见,那么在Activity可见时,我们的三个Fragment也会成为可见的。
也许有人问为何不在这里获取Fragment的高度值然后设置容器的bottomMargin呢?因为此时Activity还没有进行measure以及layout,因此没有尺寸信息,getMeasuredHeight()和getHeight()的返回值都是0。因此我们需要在Activity的ViewTree至少进行了一遍measure和layout之后才能拿到尺寸信息。
那这个时机是什么时候呢?我们需要知道一个很有用的监听器,它就是用来监听何时ViewTree完成layout的。
- 监听ViewTree布局完成并设置Fragment参数
//在onCreate中调用 getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this); Observable.timer(1000, TimeUnit.MILLISECONDS, Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer() { @Override public void accept(Long aLong) throws Exception { if(!mIsLoggedIn && !shouldShowLoginFragment) { loadWelcomeFragment(); }else if(!mIsLoggedIn && shouldShowLoginFragment) { loadRegistrationFragment(); }else{ loadMainFragment(); } } }); } });
我们通过DecorView(它是一个Activity的根View)来添加监听器。在监听器被触发之后就移除它,因为我们只需要一次监听。然后根据状态来决定显示哪一个Fragment。显示Fragment的函数举一例,包含了动画部分以及初始化:
private void loadWelcomeFragment() { showWelcomeFragment(); dismissRegFragment(); dismissMainFragment(); } private void showWelcomeFragment() { log.e("show welcome fragment called"); if(binding.flWelcomeFragmentContainer.getVisibility() != View.VISIBLE) { initWelcomeFragment(); } if(welFragAnimator == null) { initWelAnimator(); } ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)binding.flWelcomeFragmentContainer.getLayoutParams(); if(params.bottomMargin == 0) { return; } welFragAnimator.start(); } private void dismissWelcomeFragment() { log.e("dismiss welcome fragment called"); if(welFragAnimator == null) { initWelAnimator(); } ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)binding.flWelcomeFragmentContainer.getLayoutParams(); if(params.bottomMargin == -binding.flWelcomeFragmentContainer.getHeight()) { return; } welFragAnimator.reverse(); }
显然,如果我们判断对应Fragment的layout不是可见的,那么说明这个Fragment的位置我们还没有做好初始化,那么久进去做初始化工作。初始化Fragment以及对应的动画如下:
private void initWelcomeFragment() { if(binding.flWelcomeFragmentContainer.getVisibility() != View.VISIBLE) { int height = mWelcomeFragemtn.getView().getHeight(); log.d("welcome frag container height = " + height); ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) binding.flWelcomeFragmentContainer.getLayoutParams(); params.bottomMargin = -height; binding.flWelcomeFragmentContainer.setLayoutParams(params); binding.flWelcomeFragmentContainer.setVisibility(View.VISIBLE); } } private void initWelAnimator() { if(welFragAnimator == null) { welFragAnimator = ValueAnimator.ofInt(binding.flWelcomeFragmentContainer.getHeight(), 0); welFragAnimator.addUpdateListener(welFragAnimatorListener); welFragAnimator.setDuration(ANIMATION_DURATION); } }
至此,在这个地方我们终于可以放心拿到正确的高度值了。并且用这个高度值来设置Fragment的布局和动画参数了。
完美!
完美???
那标题怎么办?
其实并不完美,因为在实际运行的时候,有一个Fragment的滑上滑下的动画是一闪一闪的,上下抖动。其他两个正常。。。。
在排查了动画参数设置问题、变量名字没有拼写错误之后,怎么都找不到问题所在。
后来没办法,最笨的,上调试!
结果发现,抖动的那个Fragment在TreeObserver被调用时以及Activity完全显示出来这两个阶段,它会变高!!!!这就导致一开始的动画参数就是错的。
很纳闷这是怎么回事,后来终于发现它与众不同的地方就在于,其他两个最外层layout是LinearLayout和RelativeLayout,只有它用ConstraintLayout,并且这个ConstraintLayout还是wrap_content的,暂且将它改成固定高度后,问题就消失了。。。。。
难道ConstraintLayout的measure和layout流程和别人不一样?
暂时赶时间还没来得及查看源码细细追究。后来我改成了其他布局,问题没有了,就算是wrap_content的。
暂且先记下,当做一个思路。
更多相关文章
- Android(安卓)动画系列之逐帧(Frame)动画详解
- Android(安卓)View measure流程详解
- android监听按钮的点击事件
- android 红外
- android 控件跟随手指移动,类似捕鱼达人效果
- android反射方式访问内部类成员
- android 页面切换动画效果
- Android(安卓)常用画图方法练习
- Android(安卓)PopupWindow动画效果代码