Android/Android

[안드로이드] Appcompatactivity은 어떻게 뷰 호환성을 유지할까?

_hong 2024. 11. 14. 02:46

Appcompatactivity

특정 버전 이하 기기에서 뷰 하위 호환성을 유지합니다. 즉, 특정 버전 이하에서 지원하지 않는 뷰들을 지원하기 위함입니다.

 

🧐 어떻게 Appcompatactivity는 뷰 호환성을 유지할까?

AppCompatActivity의 내부를 확인해보았습니다.

먼저, AppCompatDelegate의 installViewFactory() 함수를 호출합니다.
이를 통해 ViewFactory를 생성합니다. ViewFactory는 onCreateView
()를 통해 appcompatview로 반환해주는 역할을 합니다.

ViewFactory를 set할 때, 매개변수로 this(AppCompatDelegateImpl.kt)를 넘겨주는데 그 이유는 AppCompatDelegateImpl 클래스 자체가 LayoutInflater.Factory를 구현했기 때문입니다.

// AppCompatActivity.kt
public class AppCompatActivity extends FragmentActivity {
    private AppCompatDelegate mDelegate;
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        
        //
        // install factory from AppCompatDelegateImpl
        //
        delegate.installViewFactory(); 
        delegate.onCreate(savedInstanceState);
        super.onCreate(savedInstanceState);
    }
}

// AppCompatDelegateImpl.kt
@RestrictTo(LIBRARY)
class AppCompatDelegateImpl extends AppCompatDelegate
        implements MenuBuilder.Callback, LayoutInflater.Factory2 {
...
@Override
public void installViewFactory() {
     LayoutInflater layoutInflater = LayoutInflater.from(mContext);
     if (layoutInflater.getFactory() == null) {
         LayoutInflaterCompat.setFactory2(layoutInflater, this);
     } else {
         if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
             Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                     + " so we can not install AppCompat's");
         }
     }
}
...
}

AppCompatDelegateImpl는 LayoutInflater.Factory2의 onCreateView 함수를 구현합니다.

public interface Factory2 extends Factory {
    @Nullable
    View onCreateView(@Nullable View var1, @NonNull String var2, @NonNull Context var3, @NonNull AttributeSet var4);
  }

onCreateView를 어떻게 구현했을까?

AppCompatDelegateImpl에서 onCreateView()는 createView()를 호출합니다.

그리고 createView() 내부에선 AppCompateViewInflater가 호출 되기 이전에 Theme에서 정의된 ViewInflater가 있는지 확인을 합니다. 만약 있다면 해당 View Inflater가 사용되며, 없다면 AppCompateViewInflater가 사용됩니다.

AppCompateViewInflater를 통해 뷰 호환성이 필요한 뷰들을 수동으로 대체하여 반환합니다.
(AppCompateViewInflater는 뷰의 이름에 따라 appcompat view로 대체)

// AppCompatDelegateImpl.java

class AppCompatDelegateImpl extends AppCompatDelegate
        implements MenuBuilder.Callback, LayoutInflater.Factory2 {
  @Override
  public View createView(View parent, final String name, @NonNull Context context,
      @NonNull AttributeSet attrs) {
    if (mAppCompatViewInflater == null) {
      TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
      
      String viewInflaterClassName =
          a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
      
      // Theme에서 정의된 ViewInflater가 있다면 해당 View Inflater가 사용되며
      // 미정의한 경우 기본으로 AppCompatViewInflater를 사용
      if (viewInflaterClassName == null) {
        mAppCompatViewInflater = new AppCompatViewInflater();
      } else {
        try {
          Class<?> viewInflaterClass = Class.forName(viewInflaterClassName);
          mAppCompatViewInflater = 
              (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
                    .newInstance();
        } catch (Throwable t) {
          Log.i(TAG, "Failed to instantiate custom view inflater "
                + viewInflaterClassName + ". Falling back to default.", t);
          mAppCompatViewInflater = new AppCompatViewInflater();
        }
      }
    }    
    
    .
    .
    .
    
    return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
            IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
            true, /* Read read app:theme as a fallback at all times for legacy reasons */
            VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
    );
  }
}

만약 material 테마를 사용할 경우는 Appcompatinflater를 상속한 MaterialComponentsViewInflater에서 뷰의 이름에 따라 해당 materal 테마의 뷰로 변경됩니다.

<resources>
  <style name="Base.V14.Theme.MaterialComponents" parent="Base.V14.Theme.MaterialComponents.Bridge">
    <item name="viewInflaterClass">com.google.android.material.theme.MaterialComponentsViewInflater</item>
    ...
  </style>
  <style name="Base.V14.Theme.MaterialComponents.Light" parent="Base.V14.Theme.MaterialComponents.Light.Bridge">
    <item name="viewInflaterClass">com.google.android.material.theme.MaterialComponentsViewInflater</item>
    ...
  </style>
</resources>

<resources>
  <style name="Base.V14.Theme.MaterialComponents.Dialog" parent="Base.V14.Theme.MaterialComponents.Dialog.Bridge">
    <item name="viewInflaterClass">com.google.android.material.theme.MaterialComponentsViewInflater</item>
    ...
  </style>
  <style name="Base.V14.Theme.MaterialComponents.Light.Dialog" parent="Base.V14.Theme.MaterialComponents.Light.Dialog.Bridge">
    <item name="viewInflaterClass">com.google.android.material.theme.MaterialComponentsViewInflater</item>
      ...
  </style>
</resources>

Custom LayoutInflater

즉, 기존 코드를 변경하지않고 Custom LayoutInflater를 이용이 가능합니다. 아래의 코드처럼 AppCompatViewInflater을 상속받는 CustomViewInflater를 선언한 후, Theme에 정의

class CustomViewInflater: AppCompatViewInflater() {
        
    override fun createView(context: Context?, name: String?, attrs: AttributeSet?): View? {
        return when(name) {
            "RecyclerView" -> CustomRecyclerView(context, attrs)
            //...
            else -> super.createView(context, name, attrs)
        }
    }

    override fun createTextView(context: Context?, attrs: AttributeSet?): AppCompatTextView {
        return CustomTextView(context, attrs)
    }
}
<style name="AppTheme" parent="Base.AppTheme">
<item name="viewInflaterClass">my.app.package.CustomViewInflater</item>
</style>

 

Reference.

When to Use AppCompat Views to Write Custom Views