やっほーい
MaterialDesign2良いですねぇ、Chipとか最高じゃないすかぁ!と思って導入したらなんか見た目がおかしくなった。今回はそれについて書いていくんだよ。
問題
こういうとき、Buttonはどういう見た目になるでしょう?
android {
compileSdkVersion 28
...
}
...
dependencies {
...
implementation 'com.android.support:appcompat-v7:28.0.0-rc01'
implementation 'com.android.support:design:28.0.0-rc01'
...
}
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="buttonStyle">@style/MyButtonStyle</item>
</style>
<style name="MyButtonStyle" parent="Widget.AppCompat.Button">
<item name="android:backgroundTint">@android:color/black</item>
<item name="android:textColor">@android:color/white</item>
<item name="android:stateListAnimator">@null</item>
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="うぇーい"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
はい、正解は
こうです!
なんでそうなるのか
とその前に前提として
AppCompat系のやつって導入するとButtonとかTextViewとかいろいろなコンポーネントがAppCompatXXX
に置換される。例えば、ButtonはAppCompatButtonってな具合にね。
多分これに対応するやつに置き換わるんじゃないかなぁ
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.btn).setOnClickListener {
if (it is AppCompatButton) {
Toast.makeText(MainActivity@this, "実はAppCompatButtonなの", Toast.LENGTH_SHORT).show()
}
}
}
ちなみにドキュメントでもこのように書かれている
https://developer.android.com/reference/android/support/v7/widget/AppCompatButton
This will automatically be used when you use Button in your layouts and the top-level activity / dialog is provided by appcompat. You should only need to manually use this class when writing custom views.
寄り道だよここは
ほーん、じゃあAppCompatActivityのソースを見てみようどうなってるのか見てみたいしね☆ってなってちょっと追ってみた
https://android.googlesource.com/platform/frameworks/support/+/oreo-cts-release/v7/appcompat/src/android/support/v7/app/AppCompatActivity.java
こいつの72行目付近にある
delegate.installViewFactory();
怪しいですねぇ…実に怪しい
じゃあほら、追っかけてみようか
https://android.googlesource.com/platform/frameworks/support/+/oreo-cts-release/v7/appcompat/src/android/support/v7/app/AppCompatDelegate.java#388
388行目付近のDocコメントにそれっぽいことが書かれている。どうやらここがゴールっぽい。
でだ同じソースの195行目付近にこんなものが書かれていますねぇ…
https://android.googlesource.com/platform/frameworks/support/+/oreo-cts-release/v7/appcompat/src/android/support/v7/app/AppCompatDelegate.java#195
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
if (Build.VERSION.SDK_INT >= 24) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
それからそれから更に更に追ってみると(ゴールって言ったのは嘘だね。ゴメンねもうちょっとだけ続くんじゃ)
https://android.googlesource.com/platform/frameworks/support/+/oreo-cts-release/v7/appcompat/src/android/support/v7/app/AppCompatDelegateImplV9.java#1056
ここの1056行目にでこういうのにぶち当たる
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
で、今度はリファレンスだ
https://developer.android.com/reference/android/view/LayoutInflater#getFactory2()
Return the current LayoutInflater.Factory2. Returns null if no factory is set or the set factory does not implement the LayoutInflater.Factory2 interface. This is called on each element name. If the factory returns a View, add that to the hierarchy. If it returns null, proceed to call onCreateView(name).
あぁ!それっぽいことが書かれてる
よーし!ソース見てみるぞー!
https://android.googlesource.com/platform/frameworks/support/+/oreo-cts-release/compat/java/android/support/v4/view/LayoutInflaterCompat.java
でもAppCompatActivityを使うと onCreate
でLayoutInflater
の何かが差し替わってAppCompatXXX
が提供されるってことは雰囲気でなんとなくわかった。
【追記】
あった、ありました!まさにこれが見たかった!しかもすごくわかりやすい!
最近のSupport Libraryで(Activity)view.getContext()するとクラッシュするので注意
https://qiita.com/takahirom/items/aa9b981a77d7c3ae0c64
なるほどAppCompatViewInflaterってやつが置き換えてるのか…
でもう一回追いかけて見たらそもそも
https://android.googlesource.com/platform/frameworks/support/+/oreo-cts-release/v7/appcompat/src/android/support/v7/app/AppCompatDelegate.java#94
これがFactory2を実装していて、LayoutInflaterCompat.setFactory2(layoutInflater, this);
this
って書かれてんのよね…
件のAppCompatViewInflaterが使われていて、そいつが差し替えを行っていた。と。はぇ〜(感嘆)
で何なのよ?
MaterialComopnentsってやつも導入したら同じようになんかしらのなにかに置き換わりが発生しているのでは?という推測ができるわけだ。
早速見てみよう
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.btn).setOnClickListener {
if (it is MaterialButton) {
Toast.makeText(MainActivity@this, "実はMaterialButtonになったの", Toast.LENGTH_SHORT).show()
}
}
}
じゃあどうすんべ
ほい、冒頭で書いた
https://developer.android.com/reference/android/support/v7/widget/AppCompatButton
This will automatically be used when you use Button in your layouts and the top-level activity / dialog is provided by appcompat. You should only need to manually use this class when writing custom views.
こいつはButtonを書くと置き換わるって書いてあったよなぁ
じゃあButtonをこうしてやれば良いわけだ。
<android.support.v7.widget.AppCompatButton
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="うぇーい"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
で、ありました!あったのです!なんか置き換えをしてそうなやつが!
https://developer.android.com/reference/com/google/android/material/theme/MaterialComponentsViewInflater
まとめ
いずれ対応するときが来るんだからさ、ButtonのスタイルとかのparentをWidget.MaterialComponents.Button
とかにして対応したほうが良いんでない?
あとリファレンスはちゃんと読まなきゃなぁと思いましたまる
ソースもちゃんと読まなきゃと思いました