1. 程式人生 > >Android中動態初始化佈局引數以及ConstraintLayout使用中遇到的坑

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

暫且先記下,當做一個思路。