[go: nahoru, domu]

blob: 66609f6f20053dfa722702b981f3b529f16cac6d [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.core
import java.text.BreakIterator
import java.util.Locale
import androidx.ui.semantics.SemanticsProperties
/**
* This class contains the implementation of text segment iterators
* for accessibility support.
*
* Note: We want to be able to iterator over [SemanticsProperties.AccessibilityLabel] of any
* component.
*/
internal class AccessibilityIterators {
interface TextSegmentIterator {
/**
* Given the current position, returning the start and end of next element in an array.
*/
fun following(current: Int): IntArray?
/**
* Given the current position, returning the start and end of previous element in an array.
*/
fun preceding(current: Int): IntArray?
}
abstract class AbstractTextSegmentIterator : TextSegmentIterator {
protected lateinit var text: String
private val segment = IntArray(2)
open fun initialize(text: String) {
this.text = text
}
protected fun getRange(start: Int, end: Int): IntArray? {
if (start < 0 || end < 0 || start == end) {
return null
}
segment[0] = start
segment[1] = end
return segment
}
}
open class CharacterTextSegmentIterator private constructor(locale: Locale) :
AbstractTextSegmentIterator() {
companion object {
private var instance: CharacterTextSegmentIterator? = null
fun getInstance(locale: Locale): CharacterTextSegmentIterator {
if (instance == null) {
instance = CharacterTextSegmentIterator(locale)
}
return instance as CharacterTextSegmentIterator
}
}
private lateinit var impl: BreakIterator
init {
onLocaleChanged(locale)
// TODO(yingleiw): register callback for locale change
// ViewRootImpl.addConfigCallback(this);
}
override fun initialize(text: String) {
super.initialize(text)
impl.setText(text)
}
override fun following(current: Int): IntArray? {
val textLength = text.length
if (textLength <= 0) {
return null
}
if (current >= textLength) {
return null
}
var start = current
if (start < 0) {
start = 0
}
while (!impl.isBoundary(start)) {
start = impl.following(start)
if (start == BreakIterator.DONE) {
return null
}
}
val end = impl.following(start)
if (end == BreakIterator.DONE) {
return null
}
return getRange(start, end)
}
override fun preceding(current: Int): IntArray? {
val textLength = text.length
if (textLength <= 0) {
return null
}
if (current <= 0) {
return null
}
var end = current
if (end > textLength) {
end = textLength
}
while (!impl.isBoundary(end)) {
end = impl.preceding(end)
if (end == BreakIterator.DONE) {
return null
}
}
val start = impl.preceding(end)
if (start == BreakIterator.DONE) {
return null
}
return getRange(start, end)
}
// TODO(yingleiw): callback for locale change
/*
@Override
public void onConfigurationChanged(Configuration globalConfig) {
final Locale locale = globalConfig.getLocales().get(0);
if (locale == null) {
return;
}
if (!mLocale.equals(locale)) {
mLocale = locale;
onLocaleChanged(locale);
}
}
*/
private fun onLocaleChanged(locale: Locale) {
impl = BreakIterator.getCharacterInstance(locale)
}
}
class WordTextSegmentIterator private constructor(locale: Locale) :
AbstractTextSegmentIterator() {
companion object {
private var instance: WordTextSegmentIterator? = null
fun getInstance(locale: Locale): WordTextSegmentIterator {
if (instance == null) {
instance = WordTextSegmentIterator(locale)
}
return instance as WordTextSegmentIterator
}
}
private lateinit var impl: BreakIterator
init {
onLocaleChanged(locale)
// TODO: register callback for locale change
// ViewRootImpl.addConfigCallback(this);
}
override fun initialize(text: String) {
super.initialize(text)
impl.setText(text)
}
private fun onLocaleChanged(locale: Locale) {
impl = BreakIterator.getWordInstance(locale)
}
override fun following(current: Int): IntArray? {
val textLength = text.length
if (textLength <= 0) {
return null
}
if (current >= text.length) {
return null
}
var start = current
if (start < 0) {
start = 0
}
while (!isLetterOrDigit(start) && !isStartBoundary(start)) {
start = impl.following(start)
if (start == BreakIterator.DONE) {
return null
}
}
val end = impl.following(start)
if (end == BreakIterator.DONE || !isEndBoundary(end)) {
return null
}
return getRange(start, end)
}
override fun preceding(current: Int): IntArray? {
val textLength = text.length
if (textLength <= 0) {
return null
}
if (current <= 0) {
return null
}
var end = current
if (end > textLength) {
end = textLength
}
while (end > 0 && !isLetterOrDigit(end - 1) && !isEndBoundary(end)) {
end = impl.preceding(end)
if (end == BreakIterator.DONE) {
return null
}
}
val start = impl.preceding(end)
if (start == BreakIterator.DONE || !isStartBoundary(start)) {
return null
}
return getRange(start, end)
}
private fun isStartBoundary(index: Int): Boolean {
return isLetterOrDigit(index) &&
(index == 0 || !isLetterOrDigit(index - 1))
}
private fun isEndBoundary(index: Int): Boolean {
return (index > 0 && isLetterOrDigit(index - 1)) &&
(index == text.length || !isLetterOrDigit(index))
}
private fun isLetterOrDigit(index: Int): Boolean {
if (index >= 0 && index < text.length) {
val codePoint = text.codePointAt(index)
return Character.isLetterOrDigit(codePoint)
}
return false
}
}
class ParagraphTextSegmentIterator : AbstractTextSegmentIterator() {
companion object {
private var instance: ParagraphTextSegmentIterator? = null
fun getInstance(): ParagraphTextSegmentIterator {
if (instance == null) {
instance = ParagraphTextSegmentIterator()
}
return instance as ParagraphTextSegmentIterator
}
}
override fun following(current: Int): IntArray? {
val textLength = text.length
if (textLength <= 0) {
return null
}
if (current >= textLength) {
return null
}
var start = current
if (start < 0) {
start = 0
}
while (start < textLength && text[start] == '\n' &&
!isStartBoundary(start)) {
start++
}
if (start >= textLength) {
return null
}
var end = start + 1
while (end < textLength && !isEndBoundary(end)) {
end++
}
return getRange(start, end)
}
override fun preceding(current: Int): IntArray? {
val textLength = text.length
if (textLength <= 0) {
return null
}
if (current <= 0) {
return null
}
var end = current
if (end > textLength) {
end = textLength
}
while (end > 0 && text[end - 1] == '\n' && !isEndBoundary(end)) {
end--
}
if (end <= 0) {
return null
}
var start = end - 1
while (start > 0 && !isStartBoundary(start)) {
start--
}
return getRange(start, end)
}
private fun isStartBoundary(index: Int): Boolean {
return (text[index] != '\n' &&
(index == 0 || text[index - 1] == '\n'))
}
private fun isEndBoundary(index: Int): Boolean {
return (index > 0 && text[index - 1] != '\n' &&
(index == text.length || text[index] == '\n'))
}
}
// TODO: This is tightly coupled with Text.kt. Need to discuss with the text team on how to
// expose the necessary properties.
/*
static class LineTextSegmentIterator extends AbstractTextSegmentIterator {
private static LineTextSegmentIterator sLineInstance;
protected static final int DIRECTION_START = -1;
protected static final int DIRECTION_END = 1;
protected Layout mLayout;
public static LineTextSegmentIterator getInstance() {
if (sLineInstance == null) {
sLineInstance = new LineTextSegmentIterator();
}
return sLineInstance;
}
public void initialize(Spannable text, Layout layout) {
mText = text.toString();
mLayout = layout;
}
@Override
public int[] following(int offset) {
final int textLegth = mText.length();
if (textLegth <= 0) {
return null;
}
if (offset >= mText.length()) {
return null;
}
int nextLine;
if (offset < 0) {
nextLine = mLayout.getLineForOffset(0);
} else {
final int currentLine = mLayout.getLineForOffset(offset);
if (getLineEdgeIndex(currentLine, DIRECTION_START) == offset) {
nextLine = currentLine;
} else {
nextLine = currentLine + 1;
}
}
if (nextLine >= mLayout.getLineCount()) {
return null;
}
final int start = getLineEdgeIndex(nextLine, DIRECTION_START);
final int end = getLineEdgeIndex(nextLine, DIRECTION_END) + 1;
return getRange(start, end);
}
@Override
public int[] preceding(int offset) {
final int textLegth = mText.length();
if (textLegth <= 0) {
return null;
}
if (offset <= 0) {
return null;
}
int previousLine;
if (offset > mText.length()) {
previousLine = mLayout.getLineForOffset(mText.length());
} else {
final int currentLine = mLayout.getLineForOffset(offset);
if (getLineEdgeIndex(currentLine, DIRECTION_END) + 1 == offset) {
previousLine = currentLine;
} else {
previousLine = currentLine - 1;
}
}
if (previousLine < 0) {
return null;
}
final int start = getLineEdgeIndex(previousLine, DIRECTION_START);
final int end = getLineEdgeIndex(previousLine, DIRECTION_END) + 1;
return getRange(start, end);
}
protected int getLineEdgeIndex(int lineNumber, int direction) {
final int paragraphDirection = mLayout.getParagraphDirection(lineNumber);
if (direction * paragraphDirection < 0) {
return mLayout.getLineStart(lineNumber);
} else {
return mLayout.getLineEnd(lineNumber) - 1;
}
}
}
static class PageTextSegmentIterator extends LineTextSegmentIterator {
private static PageTextSegmentIterator sPageInstance;
private TextView mView;
private final Rect mTempRect = new Rect();
public static PageTextSegmentIterator getInstance() {
if (sPageInstance == null) {
sPageInstance = new PageTextSegmentIterator();
}
return sPageInstance;
}
public void initialize(TextView view) {
super.initialize((Spannable) view.getIterableTextForAccessibility(), view.getLayout());
mView = view;
}
@Override
public int[] following(int offset) {
final int textLength = mText.length();
if (textLength <= 0) {
return null;
}
if (offset >= mText.length()) {
return null;
}
if (!mView.getGlobalVisibleRect(mTempRect)) {
return null;
}
final int start = Math.max(0, offset);
final int currentLine = mLayout.getLineForOffset(start);
final int currentLineTop = mLayout.getLineTop(currentLine);
final int pageHeight = mTempRect.height() - mView.getTotalPaddingTop()
- mView.getTotalPaddingBottom();
final int nextPageStartY = currentLineTop + pageHeight;
final int lastLineTop = mLayout.getLineTop(mLayout.getLineCount() - 1);
final int currentPageEndLine = (nextPageStartY < lastLineTop)
? mLayout.getLineForVertical(nextPageStartY) - 1 : mLayout.getLineCount() - 1;
final int end = getLineEdgeIndex(currentPageEndLine, DIRECTION_END) + 1;
return getRange(start, end);
}
@Override
public int[] preceding(int offset) {
final int textLength = mText.length();
if (textLength <= 0) {
return null;
}
if (offset <= 0) {
return null;
}
if (!mView.getGlobalVisibleRect(mTempRect)) {
return null;
}
final int end = Math.min(mText.length(), offset);
final int currentLine = mLayout.getLineForOffset(end);
final int currentLineTop = mLayout.getLineTop(currentLine);
final int pageHeight = mTempRect.height() - mView.getTotalPaddingTop()
- mView.getTotalPaddingBottom();
final int previousPageEndY = currentLineTop - pageHeight;
int currentPageStartLine = (previousPageEndY > 0) ?
mLayout.getLineForVertical(previousPageEndY) : 0;
// If we're at the end of text, we're at the end of the current line rather than the
// start of the next line, so we should move up one fewer lines than we would otherwise.
if (end == mText.length() && (currentPageStartLine < currentLine)) {
currentPageStartLine += 1;
}
final int start = getLineEdgeIndex(currentPageStartLine, DIRECTION_START);
return getRange(start, end);
}
}
*/
}