しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

【Kotlin】res/rawフォルダとassetsフォルダの動作を見る

res/rawフォルダとassetsフォルダの違いが気になったので動作を見てみました。

公式ドキュメントの記述は下の通りです。
どちらも未加工のもの(おそらくtxtファイルなど)を入れるためのフォルダになります。
res/rawだとリソースIDでアクセスできるけどassetsだとアクセスできないという違いのようです。

未加工の形式で保存する任意のファイル。このリソースを未加工の InputStream で開くには、リソース ID(R.raw.filename)を指定して Resources.openRawResource() を呼び出します。

ただし、元のファイル名とファイル階層にアクセスする必要がある場合は、リソースの一部を res/raw/ ではなく assets/ ディレクトリに保存することもできます。assets/ に保存されたファイルにはリソース ID が付けられません。そのため、このファイルは AssetManager を使用する場合にだけ読み取ることができます。

アプリのリソースの概要  |  Android デベロッパー  |  Android Developers

それでは上記を実際に試してみようと思います。

まずは新規プロジェクトを作成してres/rawフォルダにhello.txtを入れます。
hello.txtの中身は下の通りです。

Hello world

続いてそのファイルにアクセスします。
Activityからのアクセス方法は下の通りです。
openRawResourceを呼ぶことでInputStreamを取得できます。

resources.openRawResource(R.raw.hello)

hello.txtの中身の取得方法は下の通りです。
InputStreamからInputStreamReaderを作って中身を取り出しています。

resources.openRawResource(R.raw.hello).use { input ->
    val text = InputStreamReader(input).use { it.readText() }
    Log.d("test", text) // → Hello world
}

次はassetsフォルダのデータ取得をしていきます。
まずはresフォルダと同じ階層にassetsフォルダを配置します。
配置するとAndroidStudio上では下のように表示されます。
これをassetなど別の名前にするとAndroidStudio上では見れません。

assetsには下のような内容のhello2.txtを配置します。

Hello world2

Activityからのアクセスは下のように行います。
ファイル名を文字列で指定するので動的なファイル指定などに便利そうです。
ファイル名を間違えた場合はFileNotFoundExceptionになるので注意が必要です。

assets.open("hello2.txt").use { input ->
    val text = InputStreamReader(input).use { it.readText() }
    Log.d("test", text) // → Hello world2
}

一応res/rawも下のようにすれば文字列からリソース取得可能です。

val resourceId = resources.getIdentifier("hello", "raw", packageName)
resources.openRawResource(resourceId).use { input ->
    val text = InputStreamReader(input).use { it.readText() }
    Log.d("test", text) // → Hello world
}

assetsは下のように中にフォルダを入れれるというメリットがあります。
res/rawの場合は中にフォルダを作っても認識されません。

Androidアプリ開発でダークモード対応とローカライズを両立する方法を調べてみた

Androidアプリのリソースファイルですがvalues-nightというフォルダに入れる事でダークモード用リソースにする事が可能です。
同じようにvalues-jaのようなフォルダに入れる事で多言語対応ができます。
あまりないとは思うのですが、海外対応&ダークモード対応を同時にしたい場合にどうすれば良いか調べてみました。

結論としてはvalues-ja-nightといったフォルダに入れる事で日本語のダークモード用のリソースファイルを作る事ができました。
実際にcolors.xmlに色を入れて実験してみます。

まずはvalues/color.xmlに下のような色を定義します。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="test">#FF6666</color>
</resources>

その色をMainActivityの背景色に設定します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
    android:background="@color/test"
    tools:context=".MainActivity">

<!-- 略 -->

</androidx.coordinatorlayout.widget.CoordinatorLayout>

この状態でアプリ起動すると先程作成した色が背景色になっています。

次はvalues-ja-night/colors.xmlに下のように色を入れます。
フォルダ名ですがvalues-night-jaのように逆にするとInvalid resource directory nameというエラーが出ます。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="test">#666666</color>
</resources>

シミュレータを日本語&ダークモードにして起動すると新たに設定した色が反映されます。

設定からダークモードOFFにしたり言語を日本語以外にするとvalues/color.xmlが再び反映されるようになります。

values-ja-night/colors.xmlでtestを消した場合もvalues/color.xmlの色が反映されます。

<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--    <color name="test">#666666</color>-->
</resources>

最後にvalues-nightvalues-jaの両方がある場合にどうなるかも検証します。
values-ja-nightが一番優先されるので、事前にこのファイルは消しておきます。
values-nightvalues-jaはそれぞれ下のように色を設定しました。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="test">#66FF66</color>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="test">#6666FF</color>
</resources>

アプリを起動した所、values-jaのほうが優先されました。

【Kotlin】AndroidのActionViewを試してみる

ActionViewとはアプリバー上に色々なViewを表示できる機能です。
ボタンを押したらアプリバーに検索用の文字入力を表示するといった事ができるようになります。
今回は下URLを参考に試しました。

アクション ビューとアクション プロバイダの使用  |  Android デベロッパー  |  Android Developers

まずは新規プロジェクトを作成します。

アプリを作成したらmenu_main.xmlを下のように変更します。

<menu 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"
    tools:context="com.example.myapplication.MainActivity">
    <item
        android:id="@+id/action_search"
        android:title="検索"
        app:showAsAction="ifRoom|collapseActionView"
        app:actionViewClass="android.widget.SearchView" />
</menu>

アプリを起動すると下のような表示になります。

検索ボタンを押すと下のように検索画面になります。
この検索アイコンを押すと文字入力も可能です。

xmlで設定したshowAsActionのifRoomとcollapseActionViewについても見ていきます。
ifRoomはスペースがあればメニューをアプリバーに表示するというものです。
ifRoomを外すと下のように右上のボタン内に収納されます。

ifRoomを指定しても入り切らない場合は下のように収納されます。

collapseActionViewはActionViewの折りたたみが可能になるオプションです。
これがないと下のように「←」ボタンが表示されなくなります。

この辺りのオプションは下URLを参考にしました。

メニュー リソース  |  Android デベロッパー  |  Android Developers

次は検索フォームで文字入力した時のイベントを取ってみます。

SearchViewの取得方法は下の通りです。
onCreateOptionsMenu内でmenuからアイテムを取得、そのアイテムからActionViewを取得しています。

override fun onCreateOptionsMenu(menu: Menu): Boolean {
    menuInflater.inflate(R.menu.menu_main, menu)
    val searchView = menu.findItem(R.id.action_search).actionView as? SearchView
    searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
        override fun onQueryTextChange(newText: String?): Boolean {
            Log.d("TEST", "onQueryTextChange")
            return true
        }

        override fun onQueryTextSubmit(query: String?): Boolean {
            Log.d("TEST", "onQueryTextSubmit")
            return true
        }
    })
    return true
}

ActionViewでは下のような独自クラスを指定することも可能です。

class MyClass(context: Context): androidx.appcompat.widget.AppCompatTextView(context) {
    init {
        text = "テスト"
    }
}

xml側では下のように指定します。

<menu 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"
    tools:context="com.example.myapplication.MainActivity">
    <item
        android:id="@+id/action_search"
        android:title="検索"
        app:showAsAction="ifRoom|collapseActionView"
        app:actionViewClass="com.example.myapplication.MyClass" />
</menu>

実行するとアプリバーに先程作ったTextViewを表示できます。

actionLayoutという属性を使うとlayoutファイルを指定する事も可能です。
app:actionLayoutではなくandroid:actionLayoutにすると何も表示されないので注意が必要です。

<menu 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"
    tools:context="com.example.myapplication.MainActivity">
    <item
        android:id="@+id/action_search"
        android:title="検索"
        app:showAsAction="ifRoom|collapseActionView"
        app:actionLayout="@layout/test" />
</menu>

layout/testの中身は下の通りです。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#999">

    <TextView
        android:id="@+id/action_view_text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="テスト"
        android:background="#999" />
</LinearLayout>

実行してボタンを押すと下のように表示されます。

ActionViewのレイアウトのTextViewは下のように取得できます。

override fun onCreateOptionsMenu(menu: Menu): Boolean {
    menuInflater.inflate(R.menu.menu_main, menu)
    val actionView = menu.findItem(R.id.action_search).actionView
    val textView = actionView?.findViewById<TextView>(R.id.action_view_text)
    return true
}

AnimationDrawableでアニメーションを作る

AnimationDrawableでアニメーションを作れるようなので試してみました。

プロジェクトを作ったらまず下のようなアニメーション画像2枚を追加します。

次はdrawableフォルダ内に下のようなtest_animation.xmlを作成します。
android:oneshotは1回だけか繰り返すかを決めるものでandroid:durationはアニメーションの時間(ミリ秒単位)になります。

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="true">
    <item android:drawable="@drawable/test1" android:duration="200" />
    <item android:drawable="@drawable/test2" android:duration="200" />
</animation-list>

次にレイアウトファイルでImageViewを設置します。
背景画像は先程作成したアニメーションファイルにします。

<ImageView
    android:id="@+id/image_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/test_animation" />

最後にアニメーションをスタートする処理を書けば完了です。
isRunningの時はアニメーションを開始できないので、毎回stopしています。

binding.imageView.setOnClickListener {
    val animate = it.background as? AnimationDrawable
    if (animate!!.isRunning)
        animate.stop()
    animate?.start()
}

【Kotlin】XML用の独自の属性を定義する

下のmyTextのように自分独自の属性を定義して使う方法を調べてみました。

<TextView
    app:myText="Test" />

定義res/valuesにattrs.xmlに下のように記載するだけです。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyAttributes">
        <attr name="myText" format="string" />
        <attr name="myValue" format="integer" />
    </declare-styleable>
</resources>

あとはレイアウトファイルにxmlns:app="http://schemas.android.com/apk/res-auto"を追加すればapp:myTextでアクセスできるようになります。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_first_fragment"
        app:myText="test" />
</LinearLayout>

属性の型はstringやinteger以外にもbooleanやcolorなど様々なものがあります。
レイアウト以外ではこの型に合う値以外を入れるとエラーが出るようになります。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyAttributes">
        <attr name="myValue2" format="boolean" />
        <attr name="myValue3" format="color" />
        <attr name="myValue4" format="enum">
            <enum name="test1" value="1" />
            <enum name="test2" value="2" />
        </attr>
    </declare-styleable>
</resources>

この属性ですが下のように独自のViewを作った時に活用できます。

<com.example.myapplication.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:myText="test" />

セットした属性は下の形で取得できます。

class CustomView(context: Context, attributeSet: AttributeSet) : LinearLayout(context, attributeSet) {
    init {
        val styledAttrs = context.obtainStyledAttributes(attributeSet, R.styleable.MyAttributes)
        val textView = TextView(context).apply {
            text = styledAttrs.getString(R.styleable.MyAttributes_myText)
        }
        addView(textView)
    }
}

文字列を無理やりIntなどにしようとするとandroid.view.InflateExceptionでクラッシュします。

styledAttrs.getString(R.styleable.MyAttributes_myText) // → test
styledAttrs.getInt(R.styleable.MyAttributes_myText, -1) // → クラッシュ
styledAttrs.getBoolean(R.styleable.MyAttributes_myText, false) // → クラッシュ

ただ数字を入れていた場合は普通に取得する事が可能です。

<com.example.myapplication.CustomView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:myText="1" />

XMLでセットしてない場合はnullが返ってきます。
IntやBoolの場合はデフォルト値が返ってきます。

<com.example.myapplication.CustomView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

Androidアプリのスプラッシュ画面を実装する

アプリ起動時に表示されるスプラッシュ画面の実装を試してみました。
AndroidOS12から仕様が変わったようなので両方のパターンで作ってみます。

OS11以前

下の記事を参考に試してみました。
まずはwindowBackgroundを使う形で試してみます。

qiita.com

まずはdrawableにスプラッシュにしたいリソースを追加します。

次にスプラッシュ表示用のStyleを作成します。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Theme.MyApplication.TestStyle">
        <item name="android:windowBackground">@drawable/ic_android_black_24dp</item>
    </style>
</resources>

Styleを作ったらAndroidManifest.xmlでActivityのテーマに設定します。

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:label="@string/app_name"
    android:theme="@style/Theme.MyApplication.TestStyle">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

最後にActivity起動時に元々のThemeに変えるように設定します。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setTheme(R.style.Theme_MyApplication)

    }
}

縦横比はおかしいですがアプリ起動時にイラストを表示できました。
ちなみにActivity内でThemeを変えないとずっと背景にスプラッシュが出ることになります。

上記以外ですとActivityやFragmentを作ってアプリ起動時に一瞬表示させる方法などがあります。
この方法ですと動的な処理を入れる事も可能です。
今回は参考記事にあったようにActivityでスプラッシュを表示する方法を試してみます。

まずはbuild.gradleに一瞬スプラッシュを表示する時に使うライブラリを追加します。

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
}

次にSplashActivityを作成します。

スプラッシュ用にSplashActivityのレイアウトを整えます。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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=".SplashActivity">

    <ImageView
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:src="@drawable/ic_android_black_24dp"
        android:layout_gravity="center" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

SplashActivityを作れたらAndroidManifest.xmlで起動時ActivityをSplashActivityに変更します。

<activity
    android:name=".SplashActivity"
    android:exported="true"
    android:label="@string/title_activity_splash"
    android:theme="@style/Theme.MyApplication.NoActionBar">

    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

最後にSplashActivityに一瞬表示されたらMainActivityを表示する処理を追加します。

class SplashActivity : AppCompatActivity() {
    private lateinit var binding: ActivitySplashBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivitySplashBinding.inflate(layoutInflater)
        setContentView(binding.root)

        CoroutineScope(Dispatchers.Main).launch {
            delay(500)
            val intent = Intent(this@SplashActivity, MainActivity::class.java)
            startActivity(intent)
            finish()
        }
    }
}

これで一瞬SplashActivityが出てから消えるようになりました。
ただ、こちらの方法ではスプラッシュが表示される前に一瞬白い画面が出るので注意が必要です。
windowBackgroundですと起動とほぼ同時にスプラッシュを出す事ができます。

OS12以降

次はOS12以降用のSplashScreen APIを使った方法を調べていきます。
こちらは下ページを参考にしました。

developer.android.com

OS12ですがデフォルトでスプラッシュ画像が表示されるようになります。
スプラッシュ画像は画面の真ん中にアプリアイコンがセットされたようなものです。

このスプラッシュ画像の背景色はandroid:windowSplashScreenBackgroundという要素を入れる事で変更できます。

<resources xmlns:tools="http://schemas.android.com/tools">
    <style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <item name="android:windowSplashScreenBackground" tools:targetApi="S">@color/teal_200</item>
    </style>
</resources>

アイコンとアイコンの背景色はwindowSplashScreenAnimatedIconとwindowSplashScreenIconBackgroundColorで変更します。
AnimatedVectorDrawableを指定すればアニメーション付きアイコンを表示する事も可能です。

<resources xmlns:tools="http://schemas.android.com/tools">
    <style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="S">@drawable/animation_icon</item>
        <item name="android:windowSplashScreenIconBackgroundColor" tools:targetApi="S">@color/teal_200</item>
    </style>
</resources>

アイコンですが下のようにサイズに関する制限があるので注意が必要です。

ブランド イメージ: 200 × 80 dp にする必要があります。
背景があるアプリアイコン: 直径 160 dp の円に収まる 240 x 240 dp にする必要があります。
背景がないアプリアイコン: 直径が 192 dp の円に収まる 288 x 288 dp にする必要があります。

スプラッシュの表示を伸ばしたい場合、addOnPreDrawListenerを利用します。
処理が終わるまで描画を止める事でスプラッシュを表示し続ける事ができます。

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val content = findViewById<View>(android.R.id.content)
        content.viewTreeObserver.addOnPreDrawListener(
            object : ViewTreeObserver.OnPreDrawListener {
                override fun onPreDraw(): Boolean {
                    // Check if the initial data is ready.
                    return if (/* 判定式を入れる */) {
                        // The content is ready; start drawing.
                        content.viewTreeObserver.removeOnPreDrawListener(this)
                        true
                    } else {
                        // The content is not ready; suspend.
                        false
                    }
                }
            }
        )
    }
}

Androidアプリ開発のThemeとStyleについて調べてみた

ThemeとStyleの違いについてよく分からなかったので調べてみました。

下記事によるとThemeはアプリ全体など広範囲に適用するものでStyleは個別のパーツに設定するもののようです。
記事を見た感じApplicationやActivityにはThemeを利用、Buttonなどの個別のViewにはStyleを使っているようでした。

developer.android.com

試しにStyleを作ってTextViewにセットしてみました。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="TestStyle">
        <item name="android:textColor">#00FF00</item>
    </style>
</resources>
<TextView
    android:id="@+id/textview_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_first_fragment"
    style="@style/TestStyle"
    app:layout_constraintBottom_toTopOf="@id/button_first"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

アプリを起動した所、無事に色変更が反映されました。

このStyleですがthemes.xmlに移動しても問題なく動きました。

<style name="TestStyle">
    <item name="android:textColor">#00FF00</item>
</style>

逆にstyleをandroid:themeで指定した場合も動かす事ができました。

<TextView
    android:id="@+id/textview_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_first_fragment"
    android:theme="@style/TestStyle"
    app:layout_constraintBottom_toTopOf="@id/button_first"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

優先度を調べるためにandroid:themeとstyleの両方にセットした所styleが優先されるようでした。

<resources>
    <style name="TestStyle">
        <item name="android:textColor">#00FF00</item>
    </style>

    <style name="TestStyle2">
        <item name="android:textColor">#0000FF</item>
    </style>
</resources>
<TextView
    android:id="@+id/textview_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_first_fragment"
    android:theme="@style/TestStyle2"
    style="@style/TestStyle"
    app:layout_constraintBottom_toTopOf="@id/button_first"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

android:themeですがTextViewの親Viewにセットしても反映されました。
ただstyleは反映されませんでした。

<androidx.constraintlayout.widget.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"
    android:theme="@style/TestStyle"
    style="@style/TestStyle2"
    tools:context=".FirstFragment">
</androidx.constraintlayout.widget.ConstraintLayout>

親と子の両方にandroid:themeをセットした場合、子供の方が優先されます。

<androidx.constraintlayout.widget.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"
    android:theme="@style/TestStyle"
    tools:context=".FirstFragment">

    <TextView
        android:id="@+id/textview_first"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_first_fragment"
        android:theme="@style/TestStyle2"
        app:layout_constraintBottom_toTopOf="@id/button_first"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

それとTextViewではtextAppearanceという属性でもStyleをセットできます。
こちらはandroid:themeより低い優先度になります。

<TextView
    android:id="@+id/textview_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_first_fragment"
    android:textAppearance="@style/TestStyle2"
    app:layout_constraintBottom_toTopOf="@id/button_first"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

ThemeやStyleですが親の指定も可能です。
下のようにparentで親を指定します。
親の持つitemは子供側で上書きする事もできます。

<style name="TestStyle2" parent="TestStyle">
    <item name="android:textColor">#0000FF</item>
</style>

下のように親を.でつなげることでparentなしで継承できます。

<style name="TestStyle.TestStyle2">
</style>

styleですがAlertDialogなどへの設定も可能です。

<style name="MyDialogAlert" parent="android:Theme.Material.Light.Dialog.Alert">
    <item name="android:textColor">#FF0000</item>
</style>

第2引数はthemeResIdというパラメータですが、themeではなくstyleを渡すほうが良さそうです。

AlertDialog.Builder(this, R.style.MyDialogAlert).setTitle("test")
                .setPositiveButton("OK", null).show()

それとtheme内で下のように指摘すればすべてのAlertDialogにstyleを一括適用する事ができます。

<item name="android:alertDialogTheme">@style/MyDialogAlert</item>