[go: nahoru, domu]

blob: 5725820e531a5e6d8b1a1a007421ef0bf093fd59 [file] [log] [blame]
/*
* Copyright 2018 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.ui.engine.text.platform
import android.text.SpannableString
import android.text.Spanned
import android.text.TextPaint
import android.text.style.AbsoluteSizeSpan
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import android.text.style.LocaleSpan
import android.text.style.StrikethroughSpan
import android.text.style.UnderlineSpan
import androidx.annotation.VisibleForTesting
import androidx.text.LayoutCompat.ALIGN_CENTER
import androidx.text.LayoutCompat.ALIGN_LEFT
import androidx.text.LayoutCompat.ALIGN_NORMAL
import androidx.text.LayoutCompat.ALIGN_OPPOSITE
import androidx.text.LayoutCompat.ALIGN_RIGHT
import androidx.text.LayoutCompat.DEFAULT_ALIGNMENT
import androidx.text.LayoutCompat.DEFAULT_JUSTIFICATION_MODE
import androidx.text.LayoutCompat.DEFAULT_LINESPACING_MULTIPLIER
import androidx.text.LayoutCompat.DEFAULT_MAX_LINES
import androidx.text.LayoutCompat.DEFAULT_TEXT_DIRECTION
import androidx.text.LayoutCompat.JUSTIFICATION_MODE_INTER_WORD
import androidx.text.LayoutCompat.TEXT_DIRECTION_LTR
import androidx.text.LayoutCompat.TEXT_DIRECTION_RTL
import androidx.text.TextLayout
import androidx.text.style.LetterSpacingSpan
import androidx.text.style.TypefaceSpan
import androidx.ui.engine.geometry.Offset
import androidx.ui.engine.text.FontStyle
import androidx.ui.engine.text.FontSynthesis
import androidx.ui.engine.text.FontWeight
import androidx.ui.engine.text.ParagraphBuilder
import androidx.ui.engine.text.ParagraphStyle
import androidx.ui.engine.text.TextAffinity
import androidx.ui.engine.text.TextAlign
import androidx.ui.engine.text.TextDecoration
import androidx.ui.engine.text.TextDirection
import androidx.ui.engine.text.TextPosition
import androidx.ui.engine.text.hasFontAttributes
import androidx.ui.painting.Canvas
import java.util.Locale
import kotlin.math.floor
import kotlin.math.roundToInt
internal class ParagraphAndroid constructor(
val text: StringBuilder,
val paragraphStyle: ParagraphStyle,
val textStyles: List<ParagraphBuilder.TextStyleIndex>,
val typefaceAdapter: TypefaceAdapter = TypefaceAdapter()
) {
@VisibleForTesting
internal val textPaint = TextPaint(android.graphics.Paint.ANTI_ALIAS_FLAG)
private var layout: TextLayout? = null
// TODO(Migration/siyamed): width having -1 but others having 0 as default value is counter
// intuitive
var width: Float = -1.0f
get() = layout?.let { field } ?: -1.0f
val height: Float
get() = layout?.let { it.layout.height.toFloat() } ?: 0.0f
// TODO(Migration/siyamed): we do not have this concept. they limit to the max word size.
// it didn't make sense to me. I believe we might be able to do it. if we can use
// wordbreaker.
val minIntrinsicWidth: Float
get() = 0.0f
val maxIntrinsicWidth: Float
get() = layout?.let { it.maxIntrinsicWidth } ?: 0.0f
val alphabeticBaseline: Float
get() = layout?.let { it.layout.getLineBaseline(0).toFloat() } ?: Float.MAX_VALUE
// TODO(Migration/siyamed): (metrics.fUnderlinePosition - metrics.fAscent) * style.height;
val ideographicBaseline: Float
get() = Float.MAX_VALUE
val didExceedMaxLines: Boolean
get() = layout?.let { it.didExceedMaxLines } ?: false
// TODO(Migraition/haoyuchang): more getters needed to access the values in textPaint.
val textLocale: Locale
get() = textPaint.textLocale
val lineCount: Int
get() = ensureLayout.lineCount
private val ensureLayout: TextLayout
get() {
val tmpLayout = this.layout ?: throw java.lang.IllegalStateException(
"layout() should be called first"
)
return tmpLayout
}
val underlyingText: CharSequence
get() = ensureLayout.text
fun layout(width: Float, force: Boolean = false) {
val floorWidth = floor(width)
paragraphStyle.fontSize?.let {
textPaint.textSize = it
}
// TODO: This default values are problem here. If the user just gives a single font
// in the family, and does not provide any fontWeight, TypefaceAdapter will still get the
// call as FontWeight.normal (which is the default value)
if (paragraphStyle.hasFontAttributes()) {
textPaint.typeface = typefaceAdapter.create(
fontFamily = paragraphStyle.fontFamily,
fontWeight = paragraphStyle.fontWeight ?: FontWeight.normal,
fontStyle = paragraphStyle.fontStyle ?: FontStyle.normal,
fontSynthesis = paragraphStyle.fontSynthesis ?: FontSynthesis.all
)
}
paragraphStyle.locale?.let {
textPaint.textLocale = Locale(
it.languageCode,
it.countryCode ?: ""
)
}
val charSequence = applyTextStyle(text, textStyles)
val alignment = when (paragraphStyle.textAlign) {
TextAlign.LEFT -> ALIGN_LEFT
TextAlign.RIGHT -> ALIGN_RIGHT
TextAlign.CENTER -> ALIGN_CENTER
TextAlign.START -> ALIGN_NORMAL
TextAlign.END -> ALIGN_OPPOSITE
else -> DEFAULT_ALIGNMENT
}
// TODO(Migration/haoyuchang): Layout has more settings that flutter,
// we may add them in future.
val textDirectionHeuristic = when (paragraphStyle.textDirection) {
TextDirection.LTR -> TEXT_DIRECTION_LTR
TextDirection.RTL -> TEXT_DIRECTION_RTL
else -> DEFAULT_TEXT_DIRECTION
}
val maxLines = paragraphStyle.maxLines ?: DEFAULT_MAX_LINES
val justificationMode = when (paragraphStyle.textAlign) {
TextAlign.JUSTIFY -> JUSTIFICATION_MODE_INTER_WORD
else -> DEFAULT_JUSTIFICATION_MODE
}
val lineSpacingMultiplier =
paragraphStyle.lineHeight ?: DEFAULT_LINESPACING_MULTIPLIER
layout = TextLayout(
charSequence = charSequence,
width = floorWidth,
textPaint = textPaint,
alignment = alignment,
textDirectionHeuristic = textDirectionHeuristic,
lineSpacingMultiplier = lineSpacingMultiplier,
maxLines = maxLines,
justificationMode = justificationMode
)
this.width = floorWidth
}
fun getPositionForOffset(offset: Offset): TextPosition {
val line = ensureLayout.layout.getLineForVertical(offset.dy.toInt())
return TextPosition(
offset = ensureLayout.layout.getOffsetForHorizontal(line, offset.dx),
// TODO(Migration/siyamed): we provide a default value
affinity = TextAffinity.upstream
)
}
fun getLineLeft(index: Int): Float = ensureLayout.getLineLeft(index)
fun getLineRight(index: Int): Float = ensureLayout.getLineRight(index)
fun getLineHeight(index: Int): Float = ensureLayout.getLineHeight(index)
fun getLineWidth(index: Int): Float = ensureLayout.getLineWidth(index)
fun paint(canvas: Canvas, x: Float, y: Float) {
val tmpLayout = layout ?: throw IllegalStateException("paint cannot be " +
"called before layout() is called")
canvas.translate(x, y)
tmpLayout.paint(canvas.toFrameworkCanvas())
canvas.translate(-x, -y)
}
private fun applyTextStyle(
text: StringBuilder,
textStyles: List<ParagraphBuilder.TextStyleIndex>
): CharSequence {
if (textStyles.isEmpty()) return text
val spannableString = SpannableString(text)
for (textStyle in textStyles) {
val start = textStyle.start
val end = textStyle.end
val style = textStyle.textStyle
style.color?.let {
spannableString.setSpan(
ForegroundColorSpan(it.value),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
// TODO(Migration/haoyuchang): implement decorationStyle, decorationColor
style.decoration?.let {
if (it.contains(TextDecoration.underline)) {
spannableString.setSpan(
UnderlineSpan(),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
if (it.contains(TextDecoration.overline)) {
// TODO(Migration/haoyuchang): implement overline
}
if (it.contains(TextDecoration.lineThrough)) {
spannableString.setSpan(
StrikethroughSpan(),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
style.fontSize?.let {
spannableString.setSpan(
AbsoluteSizeSpan(it.roundToInt()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
if (style.hasFontAttributes()) {
val typeface = typefaceAdapter.create(
fontFamily = style.fontFamily,
fontWeight = style.fontWeight ?: FontWeight.normal,
fontStyle = style.fontStyle ?: FontStyle.normal,
fontSynthesis = style.fontSynthesis ?: FontSynthesis.all
)
spannableString.setSpan(
TypefaceSpan(typeface),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
// TODO(Migration/haoyuchang): implement textBaseLine
// TODO(Migration/haoyuchang): implement wordSpacing
// TODO(Migration/haoyuchang): support letter spacing with pixel.
style.letterSpacing?.let {
spannableString.setSpan(
LetterSpacingSpan(it),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
// TODO(Migration/haoyuchang): implement height
style.locale?.let {
spannableString.setSpan(
// TODO(Migration/haoyuchang): support locale fallback in the framework
LocaleSpan(Locale(it.languageCode, it.countryCode)),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
// TODO(Migration/haoyuchang): framework only support background color now
style.background?.let {
spannableString.setSpan(
BackgroundColorSpan(it.value),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
// TODO(Migration/haoyuchang): implement foreground or decide if we really need it
}
return spannableString
}
}