[go: nahoru, domu]

blob: edb008109a158cf11e994cf78bd77836080a3116 [file] [log] [blame]
/*
* Copyright 2020 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.text.platform.animation
import android.text.Layout
import androidx.ui.text.platform.CharSequenceCharacterIterator
import androidx.ui.text.platform.InternalPlatformTextApi
import androidx.ui.text.platform.LayoutHelper
import androidx.ui.text.platform.getLineForOffset
import java.text.BreakIterator
import java.util.Locale
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
/**
* A class represents animation segment.
*
* @param startOffset an inclusive start character offset of this segment.
* @param endOffset an exclusive end character offset of this segment.
* @param left a graphical left position from the layout origin.
* @param top a graphical top position from the layout origin.
* @param right a graphical right position from the layout origin.
* @param bottom a graphical bottom position from the layout origin.
*
* @suppress
*/
@InternalPlatformTextApi
data class Segment(
val startOffset: Int,
val endOffset: Int,
val left: Int,
val top: Int,
val right: Int,
val bottom: Int
)
/**
* Porvide a segmentation breaker for the text animation.
* @suppress
*/
@InternalPlatformTextApi
object SegmentBreaker {
private fun breakInWords(layoutHelper: LayoutHelper): List<Int> {
val text = layoutHelper.layout.text
val words = breakWithBreakIterator(text, BreakIterator.getLineInstance(Locale.getDefault()))
val set = words.toSortedSet()
for (paraIndex in 0 until layoutHelper.paragraphCount) {
val bidi = layoutHelper.analyzeBidi(paraIndex) ?: continue
val paragraphStart = layoutHelper.getParagraphStart(paraIndex)
for (i in 0 until bidi.runCount) {
set.add(bidi.getRunStart(i) + paragraphStart)
}
}
return set.toList()
}
private fun breakWithBreakIterator(text: CharSequence, breaker: BreakIterator): List<Int> {
val iter = CharSequenceCharacterIterator(text, 0, text.length)
val res = mutableListOf(0)
breaker.setText(iter)
while (breaker.next() != BreakIterator.DONE) {
res.add(breaker.current())
}
return res
}
/**
* Gets all offsets of the given segment type for animation.
*
* @param layoutHelper a layout helper
* @param segmentType a segmentation type
* @return all break offsets of the given segmentation type including 0 and text length.
*/
fun breakOffsets(layoutHelper: LayoutHelper, segmentType: SegmentType): List<Int> {
val layout = layoutHelper.layout
val text = layout.text
return when (segmentType) {
SegmentType.Document -> listOf(0, text.length)
SegmentType.Paragraph -> {
mutableListOf(0).also {
for (i in 0 until layoutHelper.paragraphCount) {
it.add(layoutHelper.getParagraphEnd(i))
}
}
}
SegmentType.Line -> {
mutableListOf(0).also {
for (i in 0 until layout.lineCount) {
it.add(layout.getLineEnd(i))
}
}
}
SegmentType.Word -> breakInWords(layoutHelper)
SegmentType.Character -> breakWithBreakIterator(
text,
BreakIterator.getCharacterInstance(Locale.getDefault()))
}
}
/**
* Break Layout into list of segments.
*
* A segment represents a unit of text animation. For example, if you specify, SegmentType
* .Line, this function will give you a list of Line segments which have line start offset and
* line end offset, and also line bounding box.
*
* The dropSpaces argument is ignored if segmentType is Document or Paragraph.
*
* If segmentType is Line and dropSpaces is true, this removes trailing spaces. If
* segmentType is Line and dropSpace is false, this use layout width as the right position of
* the line.
*
* If segmentType is Word and dropSpaces is true, this removes trailing spaces if there. If
* segmentType is Word and dropSpace is false, this includes the trailing whitespace into
* segment.
*
* If segmentType is Character and dropSpace is true, this drops whitespace only segment. If
* segmentType is Character and dropSpace is true, this include whitespace only segment.
*
* @param layoutHelper a layout helper
* @param segmentType a segmentation type
* @param dropSpaces whether dropping spacing. See function comment for more details.
* @return list of segment object
*/
fun breakSegments(
layoutHelper: LayoutHelper,
segmentType: SegmentType,
dropSpaces: Boolean
): List<Segment> {
return when (segmentType) {
SegmentType.Document -> breakSegmentWithDocument(layoutHelper)
SegmentType.Paragraph -> breakSegmentWithParagraph(layoutHelper)
SegmentType.Line -> breakSegmentWithLine(layoutHelper, dropSpaces)
SegmentType.Word -> breakSegmentWithWord(layoutHelper, dropSpaces)
SegmentType.Character -> breakSegmentWithChar(layoutHelper, dropSpaces)
}
}
private fun breakSegmentWithDocument(layoutHelper: LayoutHelper): List<Segment> {
return listOf(Segment(
startOffset = 0,
endOffset = layoutHelper.layout.text.length,
left = 0,
top = 0,
right = layoutHelper.layout.width,
bottom = layoutHelper.layout.height
))
}
private fun breakSegmentWithParagraph(layoutHelper: LayoutHelper): List<Segment> {
val result = mutableListOf<Segment>()
val layout = layoutHelper.layout
for (i in 0 until layoutHelper.paragraphCount) {
val paraStart = layoutHelper.getParagraphStart(i)
val paraEnd = layoutHelper.getParagraphEnd(i)
val paraFirstLine = layout.getLineForOffset(paraStart, false /* downstream */)
val paraLastLine = layout.getLineForOffset(paraEnd, true /* upstream */)
result.add(Segment(
startOffset = paraStart,
endOffset = paraEnd,
left = 0,
top = layout.getLineTop(paraFirstLine),
right = layout.width,
bottom = layout.getLineBottom(paraLastLine)
))
}
return result
}
private fun breakSegmentWithLine(
layoutHelper: LayoutHelper,
dropSpaces: Boolean
): List<Segment> {
val result = mutableListOf<Segment>()
val layout = layoutHelper.layout
for (i in 0 until layoutHelper.layout.lineCount) {
result.add(Segment(
startOffset = layout.getLineStart(i),
endOffset = layout.getLineEnd(i),
left = if (dropSpaces) ceil(layout.getLineLeft(i)).toInt() else 0,
top = layout.getLineTop(i),
right = if (dropSpaces) ceil(layout.getLineRight(i)).toInt() else layout.width,
bottom = layout.getLineBottom(i)
))
}
return result
}
private fun breakSegmentWithWord(
layoutHelper: LayoutHelper,
dropSpaces: Boolean
): List<Segment> {
val layout = layoutHelper.layout
val wsWidth = ceil(layout.paint.measureText(" ")).toInt()
return breakOffsets(layoutHelper, SegmentType.Word).zipWithNext { start, end ->
val lineNo = layout.getLineForOffset(start, false /* downstream */)
val paraRTL = layout.getParagraphDirection(lineNo) == Layout.DIR_RIGHT_TO_LEFT
val runRtl = layout.isRtlCharAt(start) // no bidi transition inside segment
val startPos = ceil(
layoutHelper.getHorizontalPosition(
offset = start,
usePrimaryDirection = runRtl == paraRTL,
upstream = false
)
).toInt()
val endPos = ceil(
layoutHelper.getHorizontalPosition(
offset = end,
usePrimaryDirection = runRtl == paraRTL,
upstream = true
)
).toInt()
// Drop trailing space is the line does not end with this word.
var left = min(startPos, endPos)
var right = max(startPos, endPos)
if (dropSpaces && end != 0 && layout.text.get(end - 1) == ' ') {
val lineEnd = layout.getLineEnd(lineNo)
if (lineEnd != end) {
if (runRtl) {
left += wsWidth
} else {
right -= wsWidth
}
}
}
Segment(
startOffset = start,
endOffset = end,
left = left,
top = layout.getLineTop(lineNo),
right = right,
bottom = layout.getLineBottom(lineNo)
)
}
}
private fun breakSegmentWithChar(
layoutHelper: LayoutHelper,
dropSpaces: Boolean
): List<Segment> {
val res = mutableListOf<Segment>()
breakOffsets(layoutHelper, SegmentType.Character).zipWithNext lambda@{ start, end ->
val layout = layoutHelper.layout
if (dropSpaces && end == start + 1 &&
layoutHelper.isLineEndSpace(layout.text.get(start)))
return@lambda
val lineNo = layout.getLineForOffset(start, false /* downstream */)
val paraRTL = layout.getParagraphDirection(lineNo) == Layout.DIR_RIGHT_TO_LEFT
val runRtl = layout.isRtlCharAt(start) // no bidi transition inside segment
val startPos = ceil(
layoutHelper.getHorizontalPosition(
offset = start,
usePrimaryDirection = runRtl == paraRTL,
upstream = false
)
).toInt()
val endPos = ceil(
layoutHelper.getHorizontalPosition(
offset = end,
usePrimaryDirection = runRtl == paraRTL,
upstream = true
)
).toInt()
res.add(Segment(
startOffset = start,
endOffset = end,
left = min(startPos, endPos),
top = layout.getLineTop(lineNo),
right = max(startPos, endPos),
bottom = layout.getLineBottom(lineNo)
))
}
return res
}
}