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<Long>() {
                            @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
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭