blob: 268f8115a67353718e448aaf36248ee78b14b1d5 [file] [log] [blame]
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.text.vertical.testapp
import android.graphics.Typeface
import android.os.Bundle
import android.text.Spanned
import android.text.TextPaint
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import androidx.text.vertical.VerticalTextLayout
import java.util.Locale
import kotlin.math.max
class VerticalTextSampleActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val demos =
arrayOf<Pair<String, @Composable () -> Unit>>(
"Long Text" to { ZoomableVerticalText { LongText(it) } },
"Complex Text" to { ZoomableVerticalText { ComplexText(it) } },
)
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
var selectedTabIndex by remember { mutableIntStateOf(0) }
PrimaryTabRow(selectedTabIndex = 0, modifier = Modifier.fillMaxWidth()) {
demos.forEachIndexed { index, (title, _) ->
Tab(
selected = selectedTabIndex == index,
onClick = { selectedTabIndex = index },
text = { Text(title) },
)
}
}
demos[selectedTabIndex].second()
}
}
}
}
}
@Composable
fun ZoomableVerticalText(content: @Composable (TextPaint) -> Unit) {
val fontSize = with(LocalDensity.current) { 32.sp.toPx() }
var zoom by remember { mutableFloatStateOf(1f) }
var offsetX by remember { mutableFloatStateOf(0f) }
val paint =
remember(zoom) {
TextPaint().apply {
textSize = fontSize * zoom
typeface = Typeface.SERIF
textLocale =
Locale.Builder()
.setLocale(Locale.JAPANESE)
.setUnicodeLocaleKeyword("lb", "strict")
.build()
}
}
Box(
modifier =
Modifier.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
zoom = 1f
offsetX = 0f
}
)
}
.pointerInput(Unit) {
detectTransformGestures { _, offsetChange, gestureZoom, _ ->
zoom = zoom * gestureZoom
offsetX = max(0f, offsetX + offsetChange.x)
}
}
.graphicsLayer(translationX = offsetX)
) {
content(paint)
}
}
@Composable
fun VerticalText(text: Spanned, paint: TextPaint, modifier: Modifier = Modifier) {
var vTextLayout by remember { mutableStateOf<VerticalTextLayout?>(null) }
Layout(
modifier =
modifier.fillMaxSize().drawWithContent {
drawIntoCanvas { c ->
vTextLayout?.draw(c.nativeCanvas, c.nativeCanvas.width.toFloat(), 0f)
}
},
content = {},
) { _, constraints ->
vTextLayout =
VerticalTextLayout.Builder(
text = text,
start = 0,
end = text.length,
paint = paint,
height = constraints.maxHeight.toFloat(),
)
.build()
layout(constraints.maxWidth, constraints.maxHeight) {}
}
}
@Composable
fun LongText(paint: TextPaint, modifier: Modifier = Modifier) {
VerticalText(
buildVerticalText {
text("吾輩は猫である。", mapOf("吾輩" to "わがはい", "猫" to "ねこ"))
text("名前はまだ無い。", mapOf("名前" to "なまえ", "無" to "な"))
text("\n")
text("どこで生まれたかとんと見当がつかぬ。", mapOf("見当" to "けんとう"))
text("何でも薄暗いじめじめしたところでニャーニャー泣いていた事だけは記憶している。")
text("吾輩はここで始めて人間というものを見た。")
text("しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。", mapOf("獰悪" to "どうあく"))
text("この書生というのは時々我々を捕えて煮て食うという話である。", mapOf("捕" to "つかま", "煮" to "に"))
text("しかしその当時は何という考もなかったから別段恐しいとも思わなかった。")
text("ただ彼の掌に載せられてスーと持ち上げられた時何だかフワフワした感じがあったばかりである。", mapOf("掌" to "てのひら"))
text("掌の上で少し落ちついて書生の顔を見たのがいわゆる人間というものの見始であろう。", mapOf("見始" to "みはじめ"))
text("この時妙なものだと思った感じが今でも残っている。")
text("第一毛をもって装飾されべきはずの顔がつるつるしてまるで薬缶だ。", mapOf("薬缶" to "やかん"))
text("その後猫にもだいぶ逢ったがこんな片輪には一度も出会わした事がない。", mapOf("片端" to "かたわ", "出会" to "でく"))
text("のみならず顔の真中があまりに突起している。")
text("そうしてその穴の中から時々ぷうぷうと煙を吹く。", mapOf("煙" to "けむり"))
text("どうも咽せぽくて実に弱った。", mapOf("咽" to "む"))
text("これが人間の飲む煙草というものである事はようやくこの頃知った。", mapOf("煙草" to "たばこ"))
text("\n")
},
paint,
modifier,
)
}
@Composable
fun ComplexText(paint: TextPaint, modifier: Modifier = Modifier) {
VerticalText(
buildVerticalText {
Upright("2024")
text("年の")
ruby("クリスマス") {
TateChuYoko("12")
text("月")
TateChuYoko("25")
text("日")
}
text("に")
Sideways("Google Pixel")
text("を買う。\n")
Upright("2024")
text("年は")
TateChuYoko("2024")
text("年ともかけるし")
Sideways("2024年")
text("ともかけるよ。\n")
text("もちろん")
withStyle(textColor = Color.Red) {
ruby(
buildVerticalText {
text("インライン")
withStyle(fontSize = 1.5.em) { text("スタイリング") }
}
) {
withStyle(fontSize = 0.8.em) { Sideways("inline ") }
withStyle(backgroundColor = Color.Green) { Sideways("styling") }
}
withStyle(backgroundColor = Color.LightGray) {
text("も")
withStyle(fontSize = 2.em) { text("可能") }
text("です。\n")
}
}
TateChuYoko(
buildVerticalText { // Tate Chu Yoko only respect styling.
text("2")
withStyle(backgroundColor = Color.Red) { text("0") }
withStyle(backgroundColor = Color.Green) { text("2") }
text("5")
}
)
text("年もよろしくお願いいたします。")
withFontShear { text("日本語の斜体はEnglishのItalicとは少し違います。") }
},
paint,
modifier,
)
}