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的。

暂且先记下,当做一个思路。

更多相关文章

  1. Android(安卓)动画系列之逐帧(Frame)动画详解
  2. Android(安卓)View measure流程详解
  3. android监听按钮的点击事件
  4. android 红外
  5. android 控件跟随手指移动,类似捕鱼达人效果
  6. android反射方式访问内部类成员
  7. android 页面切换动画效果
  8. Android(安卓)常用画图方法练习
  9. Android(安卓)PopupWindow动画效果代码

随机推荐

  1. 【Arduino】Arduino接收字符串
  2. Android(安卓)常用git命令
  3. iTextPdf最简单最彻底解决中文显示
  4. 我的Android进阶之旅------>Android实现
  5. Android屏幕图标尺寸规范
  6. android.support.v7.widget.TintContextW
  7. Android(安卓)SQLite 支持嵌套事务吗?
  8. android web services2
  9. Android(安卓)纯代码化编码2_基本控件
  10. 如何让Android系统或Android应用执行shel