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的。
暫且先記下,當做一個思路。