Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
weixin_43355755
engine
提交
81f219c5
E
engine
项目概览
weixin_43355755
/
engine
与 Fork 源项目一致
从无法访问的项目Fork
通知
1
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
E
engine
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
未验证
提交
81f219c5
编写于
11月 10, 2020
作者:
L
LongCatIsLooong
提交者:
GitHub
11月 10, 2020
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
[Android Text Input] Make the editing state listenable and allow batch edits (#21534)
上级
3de24322
变更
11
展开全部
隐藏空白更改
内联
并排
Showing
11 changed file
with
1688 addition
and
416 deletion
+1688
-416
ci/licenses_golden/licenses_flutter
ci/licenses_golden/licenses_flutter
+1
-0
shell/platform/android/BUILD.gn
shell/platform/android/BUILD.gn
+2
-0
shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java
...ter/embedding/engine/systemchannels/TextInputChannel.java
+45
-2
shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
...oid/io/flutter/plugin/editing/InputConnectionAdaptor.java
+133
-138
shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java
...oid/io/flutter/plugin/editing/ListenableEditingState.java
+247
-0
shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
...rm/android/io/flutter/plugin/editing/TextInputPlugin.java
+231
-172
shell/platform/android/test/README.md
shell/platform/android/test/README.md
+4
-3
shell/platform/android/test/io/flutter/FlutterTestSuite.java
shell/platform/android/test/io/flutter/FlutterTestSuite.java
+2
-0
shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java
...io/flutter/plugin/editing/InputConnectionAdaptorTest.java
+210
-75
shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java
...io/flutter/plugin/editing/ListenableEditingStateTest.java
+408
-0
shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
...d/test/io/flutter/plugin/editing/TextInputPluginTest.java
+405
-26
未找到文件。
ci/licenses_golden/licenses_flutter
浏览文件 @
81f219c5
...
...
@@ -809,6 +809,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StringCod
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/localization/LocalizationPlugin.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/mouse/MouseCursorPlugin.java
...
...
shell/platform/android/BUILD.gn
浏览文件 @
81f219c5
...
...
@@ -217,6 +217,7 @@ android_java_sources = [
"io/flutter/plugin/editing/FlutterTextUtils.java",
"io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java",
"io/flutter/plugin/editing/InputConnectionAdaptor.java",
"io/flutter/plugin/editing/ListenableEditingState.java",
"io/flutter/plugin/editing/TextInputPlugin.java",
"io/flutter/plugin/localization/LocalizationPlugin.java",
"io/flutter/plugin/mouse/MouseCursorPlugin.java",
...
...
@@ -473,6 +474,7 @@ action("robolectric_tests") {
"test/io/flutter/plugin/common/StandardMessageCodecTest.java",
"test/io/flutter/plugin/common/StandardMethodCodecTest.java",
"test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java",
"test/io/flutter/plugin/editing/ListenableEditingStateTest.java",
"test/io/flutter/plugin/editing/TextInputPluginTest.java",
"test/io/flutter/plugin/localization/LocalizationPluginTest.java",
"test/io/flutter/plugin/mouse/MouseCursorPluginTest.java",
...
...
shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java
浏览文件 @
81f219c5
...
...
@@ -675,17 +675,60 @@ public class TextInputChannel {
return
new
TextEditState
(
textEditState
.
getString
(
"text"
),
textEditState
.
getInt
(
"selectionBase"
),
textEditState
.
getInt
(
"selectionExtent"
));
textEditState
.
getInt
(
"selectionExtent"
),
textEditState
.
getInt
(
"composingBase"
),
textEditState
.
getInt
(
"composingExtent"
));
}
@NonNull
public
final
String
text
;
public
final
int
selectionStart
;
public
final
int
selectionEnd
;
public
final
int
composingStart
;
public
final
int
composingEnd
;
public
TextEditState
(
@NonNull
String
text
,
int
selectionStart
,
int
selectionEnd
,
int
composingStart
,
int
composingEnd
)
throws
IndexOutOfBoundsException
{
if
((
selectionStart
!=
-
1
||
selectionEnd
!=
-
1
)
&&
(
selectionStart
<
0
||
selectionStart
>
selectionEnd
))
{
throw
new
IndexOutOfBoundsException
(
"invalid selection: ("
+
String
.
valueOf
(
selectionStart
)
+
", "
+
String
.
valueOf
(
selectionEnd
)
+
")"
);
}
if
((
composingStart
!=
-
1
||
composingEnd
!=
-
1
)
&&
(
composingStart
<
0
||
composingStart
>=
composingEnd
))
{
throw
new
IndexOutOfBoundsException
(
"invalid composing range: ("
+
String
.
valueOf
(
composingStart
)
+
", "
+
String
.
valueOf
(
composingEnd
)
+
")"
);
}
if
(
composingStart
>
text
.
length
())
{
throw
new
IndexOutOfBoundsException
(
"invalid composing start: "
+
String
.
valueOf
(
composingStart
));
}
if
(
selectionStart
>
text
.
length
())
{
throw
new
IndexOutOfBoundsException
(
"invalid selection start: "
+
String
.
valueOf
(
selectionStart
));
}
public
TextEditState
(
@NonNull
String
text
,
int
selectionStart
,
int
selectionEnd
)
{
this
.
text
=
text
;
this
.
selectionStart
=
selectionStart
;
this
.
selectionEnd
=
selectionEnd
;
this
.
composingStart
=
composingStart
;
this
.
composingEnd
=
composingEnd
;
}
}
}
shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
浏览文件 @
81f219c5
...
...
@@ -31,68 +31,23 @@ import io.flutter.embedding.android.AndroidKeyProcessor;
import
io.flutter.embedding.engine.FlutterJNI
;
import
io.flutter.embedding.engine.systemchannels.TextInputChannel
;
class
InputConnectionAdaptor
extends
BaseInputConnection
{
class
InputConnectionAdaptor
extends
BaseInputConnection
implements
ListenableEditingState
.
EditingStateWatcher
{
private
static
final
String
TAG
=
"InputConnectionAdaptor"
;
private
final
View
mFlutterView
;
private
final
int
mClient
;
private
final
TextInputChannel
textInputChannel
;
private
final
AndroidKeyProcessor
keyProcessor
;
private
final
Editabl
e
mEditable
;
private
final
ListenableEditingStat
e
mEditable
;
private
final
EditorInfo
mEditorInfo
;
private
int
mBatchCount
;
private
ExtractedTextRequest
mExtractRequest
;
private
boolean
mMonitorCursorUpdate
=
false
;
private
CursorAnchorInfo
.
Builder
mCursorAnchorInfoBuilder
;
private
ExtractedText
mExtractedText
=
new
ExtractedText
();
private
InputMethodManager
mImm
;
private
final
Layout
mLayout
;
private
FlutterTextUtils
flutterTextUtils
;
// Used to determine if Samsung-specific hacks should be applied.
private
final
boolean
isSamsung
;
private
boolean
mRepeatCheckNeeded
=
false
;
private
TextEditingValue
mLastSentTextEditngValue
;
// Data class used to get and store the last-sent values via updateEditingState to
// the framework. These are then compared against to prevent redundant messages
// with the same data before any valid operations were made to the contents.
private
class
TextEditingValue
{
public
int
selectionStart
;
public
int
selectionEnd
;
public
int
composingStart
;
public
int
composingEnd
;
public
String
text
;
public
TextEditingValue
(
Editable
editable
)
{
selectionStart
=
Selection
.
getSelectionStart
(
editable
);
selectionEnd
=
Selection
.
getSelectionEnd
(
editable
);
composingStart
=
BaseInputConnection
.
getComposingSpanStart
(
editable
);
composingEnd
=
BaseInputConnection
.
getComposingSpanEnd
(
editable
);
text
=
editable
.
toString
();
}
@Override
public
boolean
equals
(
Object
o
)
{
if
(
o
==
this
)
{
return
true
;
}
if
(!(
o
instanceof
TextEditingValue
))
{
return
false
;
}
TextEditingValue
value
=
(
TextEditingValue
)
o
;
return
selectionStart
==
value
.
selectionStart
&&
selectionEnd
==
value
.
selectionEnd
&&
composingStart
==
value
.
composingStart
&&
composingEnd
==
value
.
composingEnd
&&
text
.
equals
(
value
.
text
);
}
@Override
public
int
hashCode
()
{
final
int
prime
=
31
;
int
result
=
1
;
result
=
prime
*
result
+
selectionStart
;
result
=
prime
*
result
+
selectionEnd
;
result
=
prime
*
result
+
composingStart
;
result
=
prime
*
result
+
composingEnd
;
result
=
prime
*
result
+
text
.
hashCode
();
return
result
;
}
}
@SuppressWarnings
(
"deprecation"
)
public
InputConnectionAdaptor
(
...
...
@@ -100,7 +55,7 @@ class InputConnectionAdaptor extends BaseInputConnection {
int
client
,
TextInputChannel
textInputChannel
,
AndroidKeyProcessor
keyProcessor
,
Editabl
e
editable
,
ListenableEditingStat
e
editable
,
EditorInfo
editorInfo
,
FlutterJNI
flutterJNI
)
{
super
(
view
,
true
);
...
...
@@ -108,8 +63,8 @@ class InputConnectionAdaptor extends BaseInputConnection {
mClient
=
client
;
this
.
textInputChannel
=
textInputChannel
;
mEditable
=
editable
;
mEditable
.
addEditingStateListener
(
this
);
mEditorInfo
=
editorInfo
;
mBatchCount
=
0
;
this
.
keyProcessor
=
keyProcessor
;
this
.
flutterTextUtils
=
new
FlutterTextUtils
(
flutterJNI
);
// We create a dummy Layout with max width so that the selection
...
...
@@ -124,8 +79,6 @@ class InputConnectionAdaptor extends BaseInputConnection {
0.0f
,
false
);
mImm
=
(
InputMethodManager
)
view
.
getContext
().
getSystemService
(
Context
.
INPUT_METHOD_SERVICE
);
isSamsung
=
isSamsung
();
}
public
InputConnectionAdaptor
(
...
...
@@ -133,52 +86,45 @@ class InputConnectionAdaptor extends BaseInputConnection {
int
client
,
TextInputChannel
textInputChannel
,
AndroidKeyProcessor
keyProcessor
,
Editabl
e
editable
,
ListenableEditingStat
e
editable
,
EditorInfo
editorInfo
)
{
this
(
view
,
client
,
textInputChannel
,
keyProcessor
,
editable
,
editorInfo
,
new
FlutterJNI
());
}
// Send the current state of the editable to Flutter.
private
void
updateEditingState
()
{
// If the IME is in the middle of a batch edit, then wait until it completes.
if
(
mBatchCount
>
0
)
return
;
TextEditingValue
currentValue
=
new
TextEditingValue
(
mEditable
);
private
ExtractedText
getExtractedText
(
ExtractedTextRequest
request
)
{
mExtractedText
.
startOffset
=
0
;
mExtractedText
.
partialStartOffset
=
-
1
;
mExtractedText
.
partialEndOffset
=
-
1
;
mExtractedText
.
selectionStart
=
mEditable
.
getSelectionStart
();
mExtractedText
.
selectionEnd
=
mEditable
.
getSelectionEnd
();
mExtractedText
.
text
=
request
==
null
||
(
request
.
flags
&
GET_TEXT_WITH_STYLES
)
==
0
?
mEditable
.
toString
()
:
mEditable
;
return
mExtractedText
;
}
// Return if this data has already been sent and no meaningful changes have
// occurred to mark this as dirty. This prevents duplicate remote updates of
// the same data, which can break formatters that change the length of the
// contents.
if
(
mRepeatCheckNeeded
&&
currentValue
.
equals
(
mLastSentTextEditngValue
))
{
return
;
private
CursorAnchorInfo
getCursorAnchorInfo
()
{
if
(
Build
.
VERSION
.
SDK_INT
<
Build
.
VERSION_CODES
.
LOLLIPOP
)
{
return
null
;
}
if
(
mCursorAnchorInfoBuilder
==
null
)
{
mCursorAnchorInfoBuilder
=
new
CursorAnchorInfo
.
Builder
();
}
else
{
mCursorAnchorInfoBuilder
.
reset
();
}
mImm
.
updateSelection
(
mFlutterView
,
currentValue
.
selectionStart
,
currentValue
.
selectionEnd
,
currentValue
.
composingStart
,
currentValue
.
composingEnd
);
textInputChannel
.
updateEditingState
(
mClient
,
currentValue
.
text
,
currentValue
.
selectionStart
,
currentValue
.
selectionEnd
,
currentValue
.
composingStart
,
currentValue
.
composingEnd
);
mRepeatCheckNeeded
=
true
;
mLastSentTextEditngValue
=
currentValue
;
}
// This should be called whenever a change could have been made to
// the value of mEditable, which will make any call of updateEditingState()
// ineligible for repeat checking as we do not want to skip sending real changes
// to the framework.
public
void
markDirty
()
{
// Disable updateEditngState's repeat-update check
mRepeatCheckNeeded
=
false
;
mCursorAnchorInfoBuilder
.
setSelectionRange
(
mEditable
.
getSelectionStart
(),
mEditable
.
getSelectionEnd
());
final
int
composingStart
=
mEditable
.
getComposingStart
();
final
int
composingEnd
=
mEditable
.
getComposingEnd
();
if
(
composingStart
>=
0
&&
composingEnd
>
composingStart
)
{
mCursorAnchorInfoBuilder
.
setComposingText
(
composingStart
,
mEditable
.
toString
().
subSequence
(
composingStart
,
composingEnd
));
}
else
{
mCursorAnchorInfoBuilder
.
setComposingText
(-
1
,
""
);
}
return
mCursorAnchorInfoBuilder
.
build
();
}
@Override
...
...
@@ -188,99 +134,112 @@ class InputConnectionAdaptor extends BaseInputConnection {
@Override
public
boolean
beginBatchEdit
()
{
m
BatchCount
++
;
m
Editable
.
beginBatchEdit
()
;
return
super
.
beginBatchEdit
();
}
@Override
public
boolean
endBatchEdit
()
{
boolean
result
=
super
.
endBatchEdit
();
mBatchCount
--;
updateEditingState
();
mEditable
.
endBatchEdit
();
return
result
;
}
@Override
public
boolean
commitText
(
CharSequence
text
,
int
newCursorPosition
)
{
boolean
result
=
super
.
commitText
(
text
,
newCursorPosition
);
markDirty
();
final
boolean
result
=
super
.
commitText
(
text
,
newCursorPosition
);
return
result
;
}
@Override
public
boolean
deleteSurroundingText
(
int
beforeLength
,
int
afterLength
)
{
if
(
Selection
.
getSelectionStart
(
mEditable
)
==
-
1
)
return
true
;
if
(
mEditable
.
getSelectionStart
()
==
-
1
)
{
return
true
;
}
boolean
result
=
super
.
deleteSurroundingText
(
beforeLength
,
afterLength
);
markDirty
();
final
boolean
result
=
super
.
deleteSurroundingText
(
beforeLength
,
afterLength
);
return
result
;
}
@Override
public
boolean
deleteSurroundingTextInCodePoints
(
int
beforeLength
,
int
afterLength
)
{
boolean
result
=
super
.
deleteSurroundingTextInCodePoints
(
beforeLength
,
afterLength
);
markDirty
();
return
result
;
}
@Override
public
boolean
setComposingRegion
(
int
start
,
int
end
)
{
boolean
result
=
super
.
setComposingRegion
(
start
,
end
);
markDirty
();
final
boolean
result
=
super
.
setComposingRegion
(
start
,
end
);
return
result
;
}
@Override
public
boolean
setComposingText
(
CharSequence
text
,
int
newCursorPosition
)
{
boolean
result
;
beginBatchEdit
();
if
(
text
.
length
()
==
0
)
{
result
=
super
.
commitText
(
text
,
newCursorPosition
);
}
else
{
result
=
super
.
setComposingText
(
text
,
newCursorPosition
);
}
markDirty
();
endBatchEdit
();
return
result
;
}
@Override
public
boolean
finishComposingText
()
{
boolean
result
=
super
.
finishComposingText
();
// Apply Samsung hacks. Samsung caches composing region data strangely, causing text
// duplication.
if
(
isSamsung
)
{
if
(
Build
.
VERSION
.
SDK_INT
>=
21
)
{
// Samsung keyboards don't clear the composing region on finishComposingText.
// Update the keyboard with a reset/empty composing region. Critical on
// Samsung keyboards to prevent punctuation duplication.
CursorAnchorInfo
.
Builder
builder
=
new
CursorAnchorInfo
.
Builder
();
builder
.
setComposingText
(
/*composingTextStart*/
-
1
,
/*composingText*/
""
);
CursorAnchorInfo
anchorInfo
=
builder
.
build
();
mImm
.
updateCursorAnchorInfo
(
mFlutterView
,
anchorInfo
);
}
}
markDirty
();
final
boolean
result
=
super
.
finishComposingText
();
return
result
;
}
// When there's not enough vertical screen space, the IME may enter fullscreen mode and this
// method will be used to get (a portion of) the currently edited text. Samsung keyboard seems
// to use this method instead of InputConnection#getText{Before,After}Cursor.
// See https://github.com/flutter/engine/pull/17426.
// TODO(garyq): Implement a more feature complete version of getExtractedText
@Override
public
ExtractedText
getExtractedText
(
ExtractedTextRequest
request
,
int
flags
)
{
ExtractedText
extractedText
=
new
ExtractedText
();
extractedText
.
selectionStart
=
Selection
.
getSelectionStart
(
mEditable
);
extractedText
.
selectionEnd
=
Selection
.
getSelectionEnd
(
mEditable
);
extractedText
.
text
=
mEditable
.
toString
();
return
extractedText
;
final
boolean
textMonitor
=
(
flags
&
GET_EXTRACTED_TEXT_MONITOR
)
!=
0
;
if
(
textMonitor
==
(
mExtractRequest
==
null
))
{
Log
.
d
(
TAG
,
"The input method toggled text monitoring "
+
(
textMonitor
?
"on"
:
"off"
));
}
// Enables text monitoring if the relevant flag is set. See
// InputConnectionAdaptor#didChangeEditingState.
mExtractRequest
=
textMonitor
?
request
:
null
;
return
getExtractedText
(
request
);
}
@Override
public
boolean
requestCursorUpdates
(
int
cursorUpdateMode
)
{
if
(
Build
.
VERSION
.
SDK_INT
<
Build
.
VERSION_CODES
.
LOLLIPOP
)
{
return
false
;
}
if
((
cursorUpdateMode
&
CURSOR_UPDATE_IMMEDIATE
)
!=
0
)
{
mImm
.
updateCursorAnchorInfo
(
mFlutterView
,
getCursorAnchorInfo
());
}
final
boolean
updated
=
(
cursorUpdateMode
&
CURSOR_UPDATE_MONITOR
)
!=
0
;
if
(
updated
!=
mMonitorCursorUpdate
)
{
Log
.
d
(
TAG
,
"The input method toggled cursor monitoring "
+
(
updated
?
"on"
:
"off"
));
}
// Enables cursor monitoring. See InputConnectionAdaptor#didChangeEditingState.
mMonitorCursorUpdate
=
updated
;
return
true
;
}
@Override
public
boolean
clearMetaKeyStates
(
int
states
)
{
boolean
result
=
super
.
clearMetaKeyStates
(
states
);
markDirty
();
return
result
;
}
@Override
public
void
closeConnection
()
{
super
.
closeConnection
();
mEditable
.
removeEditingStateListener
(
this
);
}
// Detect if the keyboard is a Samsung keyboard, where we apply Samsung-specific hacks to
// fix critical bugs that make the keyboard otherwise unusable. See finishComposingText() for
// more details.
...
...
@@ -304,9 +263,9 @@ class InputConnectionAdaptor extends BaseInputConnection {
@Override
public
boolean
setSelection
(
int
start
,
int
end
)
{
beginBatchEdit
();
boolean
result
=
super
.
setSelection
(
start
,
end
);
markDirty
();
updateEditingState
();
endBatchEdit
();
return
result
;
}
...
...
@@ -336,7 +295,6 @@ class InputConnectionAdaptor extends BaseInputConnection {
return
true
;
}
markDirty
();
if
(
event
.
getAction
()
==
KeyEvent
.
ACTION_DOWN
)
{
if
(
event
.
getKeyCode
()
==
KeyEvent
.
KEYCODE_DEL
)
{
int
selStart
=
clampIndexToEditable
(
Selection
.
getSelectionStart
(
mEditable
),
mEditable
);
...
...
@@ -349,7 +307,6 @@ class InputConnectionAdaptor extends BaseInputConnection {
// Delete the selection.
Selection
.
setSelection
(
mEditable
,
selStart
);
mEditable
.
delete
(
selStart
,
selEnd
);
updateEditingState
();
return
true
;
}
return
false
;
...
...
@@ -440,7 +397,13 @@ class InputConnectionAdaptor extends BaseInputConnection {
@Override
public
boolean
performContextMenuAction
(
int
id
)
{
markDirty
();
beginBatchEdit
();
final
boolean
result
=
doPerformContextMenuAction
(
id
);
endBatchEdit
();
return
result
;
}
private
boolean
doPerformContextMenuAction
(
int
id
)
{
if
(
id
==
android
.
R
.
id
.
selectAll
)
{
setSelection
(
0
,
mEditable
.
length
());
return
true
;
...
...
@@ -500,7 +463,6 @@ class InputConnectionAdaptor extends BaseInputConnection {
@Override
public
boolean
performEditorAction
(
int
actionCode
)
{
markDirty
();
switch
(
actionCode
)
{
case
EditorInfo
.
IME_ACTION_NONE
:
textInputChannel
.
newline
(
mClient
);
...
...
@@ -530,4 +492,37 @@ class InputConnectionAdaptor extends BaseInputConnection {
}
return
true
;
}
// -------- Start: ListenableEditingState watcher implementation -------
@Override
public
void
didChangeEditingState
(
boolean
textChanged
,
boolean
selectionChanged
,
boolean
composingRegionChanged
)
{
// This method notifies the input method that the editing state has changed.
// updateSelection is mandatory. updateExtractedText and updateCursorAnchorInfo
// are on demand (if the input method set the correspoinding monitoring
// flags). See getExtractedText and requestCursorUpdates.
// Always send selection update. InputMethodManager#updateSelection skips
// sending the message if none of the parameters have changed since the last
// time we called it.
mImm
.
updateSelection
(
mFlutterView
,
mEditable
.
getSelectionStart
(),
mEditable
.
getSelectionEnd
(),
mEditable
.
getComposingStart
(),
mEditable
.
getComposingEnd
());
if
(
Build
.
VERSION
.
SDK_INT
<
Build
.
VERSION_CODES
.
LOLLIPOP
)
{
return
;
}
if
(
mExtractRequest
!=
null
)
{
mImm
.
updateExtractedText
(
mFlutterView
,
mExtractRequest
.
token
,
getExtractedText
(
mExtractRequest
));
}
if
(
mMonitorCursorUpdate
)
{
final
CursorAnchorInfo
info
=
getCursorAnchorInfo
();
mImm
.
updateCursorAnchorInfo
(
mFlutterView
,
info
);
}
}
// -------- End: ListenableEditingState watcher implementation -------
}
shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java
0 → 100644
浏览文件 @
81f219c5
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package
io.flutter.plugin.editing
;
import
android.text.Editable
;
import
android.text.Selection
;
import
android.text.SpannableStringBuilder
;
import
android.view.View
;
import
android.view.inputmethod.BaseInputConnection
;
import
io.flutter.Log
;
import
io.flutter.embedding.engine.systemchannels.TextInputChannel
;
import
java.util.ArrayList
;
/// The current editing state (text, selection range, composing range) the text input plugin holds.
///
/// As the name implies, this class also notifies its listeners when the editing state changes. When
/// there're ongoing batch edits, change notifications will be deferred until all batch edits end
/// (i.e. when the outermost batch edit ends). Listeners added during a batch edit will always be
/// notified when all batch edits end, even if there's no real change.
///
/// Adding/removing listeners or changing the editing state in a didChangeEditingState callback may
/// cause unexpected behavior.
//
// Currently this class does not notify its listeners on spans-only changes (e.g.,
// Selection.setSelection). Wrap them in a batch edit to trigger a change notification.
class
ListenableEditingState
extends
SpannableStringBuilder
{
interface
EditingStateWatcher
{
// Changing the editing state in a didChangeEditingState callback may cause unexpected
// behavior.
void
didChangeEditingState
(
boolean
textChanged
,
boolean
selectionChanged
,
boolean
composingRegionChanged
);
}
private
static
final
String
TAG
=
"ListenableEditingState"
;
private
int
mBatchEditNestDepth
=
0
;
// We don't support adding/removing listeners, or changing the editing state in a listener
// callback for now.
private
int
mChangeNotificationDepth
=
0
;
private
ArrayList
<
EditingStateWatcher
>
mListeners
=
new
ArrayList
<>();
private
ArrayList
<
EditingStateWatcher
>
mPendingListeners
=
new
ArrayList
<>();
private
String
mToStringCache
;
private
String
mTextWhenBeginBatchEdit
;
private
int
mSelectionStartWhenBeginBatchEdit
;
private
int
mSelectionEndWhenBeginBatchEdit
;
private
int
mComposingStartWhenBeginBatchEdit
;
private
int
mComposingEndWhenBeginBatchEdit
;
private
BaseInputConnection
mDummyConnection
;
// The View is only used for creating a dummy BaseInputConnection for setComposingRegion. The View
// needs to have a non-null Context.
public
ListenableEditingState
(
TextInputChannel
.
TextEditState
configuration
,
View
view
)
{
super
();
if
(
configuration
!=
null
)
{
setEditingState
(
configuration
);
}
Editable
self
=
this
;
mDummyConnection
=
new
BaseInputConnection
(
view
,
true
)
{
@Override
public
Editable
getEditable
()
{
return
self
;
}
};
}
/// Starts a new batch edit during which change notifications will be put on hold until all batch
/// edits end.
///
/// Batch edits nest.
public
void
beginBatchEdit
()
{
mBatchEditNestDepth
++;
if
(
mChangeNotificationDepth
>
0
)
{
Log
.
e
(
TAG
,
"editing state should not be changed in a listener callback"
);
}
if
(
mBatchEditNestDepth
==
1
&&
!
mListeners
.
isEmpty
())
{
mTextWhenBeginBatchEdit
=
toString
();
mSelectionStartWhenBeginBatchEdit
=
getSelectionStart
();
mSelectionEndWhenBeginBatchEdit
=
getSelectionEnd
();
mComposingStartWhenBeginBatchEdit
=
getComposingStart
();
mComposingEndWhenBeginBatchEdit
=
getComposingEnd
();
}
}
/// Ends the current batch edit and flush pending change notifications if the current batch edit
/// is not nested (i.e. it is the last ongoing batch edit).
public
void
endBatchEdit
()
{
if
(
mBatchEditNestDepth
==
0
)
{
Log
.
e
(
TAG
,
"endBatchEdit called without a matching beginBatchEdit"
);
return
;
}
if
(
mBatchEditNestDepth
==
1
)
{
for
(
final
EditingStateWatcher
listener
:
mPendingListeners
)
{
notifyListener
(
listener
,
true
,
true
,
true
);
}
if
(!
mListeners
.
isEmpty
())
{
Log
.
v
(
TAG
,
"didFinishBatchEdit with "
+
String
.
valueOf
(
mListeners
.
size
())
+
" listener(s)"
);
final
boolean
textChanged
=
!
toString
().
equals
(
mTextWhenBeginBatchEdit
);
final
boolean
selectionChanged
=
mSelectionStartWhenBeginBatchEdit
!=
getSelectionStart
()
||
mSelectionEndWhenBeginBatchEdit
!=
getSelectionEnd
();
final
boolean
composingRegionChanged
=
mComposingStartWhenBeginBatchEdit
!=
getComposingStart
()
||
mComposingEndWhenBeginBatchEdit
!=
getComposingEnd
();
notifyListenersIfNeeded
(
textChanged
,
selectionChanged
,
composingRegionChanged
);
}
}
mListeners
.
addAll
(
mPendingListeners
);
mPendingListeners
.
clear
();
mBatchEditNestDepth
--;
}
/// Update the composing region of the current editing state.
///
/// If the range is invalid or empty, the current composing region will be removed.
public
void
setComposingRange
(
int
composingStart
,
int
composingEnd
)
{
if
(
composingStart
<
0
||
composingStart
>=
composingEnd
)
{
BaseInputConnection
.
removeComposingSpans
(
this
);
}
else
{
mDummyConnection
.
setComposingRegion
(
composingStart
,
composingEnd
);
}
}
/// Called when the framework sends updates to the text input plugin.
///
/// This method will also update the composing region if it has changed.
public
void
setEditingState
(
TextInputChannel
.
TextEditState
newState
)
{
beginBatchEdit
();
replace
(
0
,
length
(),
newState
.
text
);
if
(
newState
.
selectionStart
>=
0
&&
newState
.
selectionEnd
>=
newState
.
selectionStart
)
{
Selection
.
setSelection
(
this
,
newState
.
selectionStart
,
newState
.
selectionEnd
);
}
else
{
Selection
.
removeSelection
(
this
);
}
setComposingRange
(
newState
.
composingStart
,
newState
.
composingEnd
);
endBatchEdit
();
}
public
void
addEditingStateListener
(
EditingStateWatcher
listener
)
{
if
(
mChangeNotificationDepth
>
0
)
{
Log
.
e
(
TAG
,
"adding a listener "
+
listener
.
toString
()
+
" in a listener callback"
);
}
// It is possible for a listener to get added during a batch edit. When that happens we always
// notify the new listeners.
// This does not check if the listener is already in the list of existing listeners.
if
(
mBatchEditNestDepth
>
0
)
{
Log
.
w
(
TAG
,
"a listener was added to EditingState while a batch edit was in progress"
);
mPendingListeners
.
add
(
listener
);
}
else
{
mListeners
.
add
(
listener
);
}
}
public
void
removeEditingStateListener
(
EditingStateWatcher
listener
)
{
if
(
mChangeNotificationDepth
>
0
)
{
Log
.
e
(
TAG
,
"removing a listener "
+
listener
.
toString
()
+
" in a listener callback"
);
}
mListeners
.
remove
(
listener
);
if
(
mBatchEditNestDepth
>
0
)
{
mPendingListeners
.
remove
(
listener
);
}
}
@Override
public
SpannableStringBuilder
replace
(
int
start
,
int
end
,
CharSequence
tb
,
int
tbstart
,
int
tbend
)
{
if
(
mChangeNotificationDepth
>
0
)
{
Log
.
e
(
TAG
,
"editing state should not be changed in a listener callback"
);
}
boolean
textChanged
=
end
-
start
!=
tbend
-
tbstart
;
for
(
int
i
=
0
;
i
<
end
-
start
&&
!
textChanged
;
i
++)
{
textChanged
|=
charAt
(
start
+
i
)
!=
tb
.
charAt
(
tbstart
+
i
);
}
if
(
textChanged
)
{
mToStringCache
=
null
;
}
final
int
selectionStart
=
getSelectionStart
();
final
int
selectionEnd
=
getSelectionEnd
();
final
int
composingStart
=
getComposingStart
();
final
int
composingEnd
=
getComposingEnd
();
final
SpannableStringBuilder
editable
=
super
.
replace
(
start
,
end
,
tb
,
tbstart
,
tbend
);
if
(
mBatchEditNestDepth
>
0
)
{
return
editable
;
}
final
boolean
selectionChanged
=
getSelectionStart
()
!=
selectionStart
||
getSelectionEnd
()
!=
selectionEnd
;
final
boolean
composingRegionChanged
=
getComposingStart
()
!=
composingStart
||
getComposingEnd
()
!=
composingEnd
;
notifyListenersIfNeeded
(
textChanged
,
selectionChanged
,
composingRegionChanged
);
return
editable
;
}
private
void
notifyListener
(
EditingStateWatcher
listener
,
boolean
textChanged
,
boolean
selectionChanged
,
boolean
composingChanged
)
{
mChangeNotificationDepth
++;
listener
.
didChangeEditingState
(
textChanged
,
selectionChanged
,
composingChanged
);
mChangeNotificationDepth
--;
}
private
void
notifyListenersIfNeeded
(
boolean
textChanged
,
boolean
selectionChanged
,
boolean
composingChanged
)
{
if
(
textChanged
||
selectionChanged
||
composingChanged
)
{
for
(
final
EditingStateWatcher
listener
:
mListeners
)
{
notifyListener
(
listener
,
textChanged
,
selectionChanged
,
composingChanged
);
}
}
}
public
final
int
getSelectionStart
()
{
return
Selection
.
getSelectionStart
(
this
);
}
public
final
int
getSelectionEnd
()
{
return
Selection
.
getSelectionEnd
(
this
);
}
public
final
int
getComposingStart
()
{
return
BaseInputConnection
.
getComposingSpanStart
(
this
);
}
public
final
int
getComposingEnd
()
{
return
BaseInputConnection
.
getComposingSpanEnd
(
this
);
}
@Override
public
String
toString
()
{
return
mToStringCache
!=
null
?
mToStringCache
:
(
mToStringCache
=
super
.
toString
());
}
}
shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
浏览文件 @
81f219c5
此差异已折叠。
点击以展开。
shell/platform/android/test/README.md
浏览文件 @
81f219c5
...
...
@@ -14,9 +14,10 @@ integration tests in other repos.
`shell/platform/android/**test**/io/flutter/util/Preconditions**Test**.java`
.
2.
Add your file to the
`sources`
of the
`robolectric_tests`
build target in
`/shell/platform/android/BUILD.gn`
. This compiles the test class into the
test jar.
3.
Add your class to the
`@SuiteClasses`
annotation in
`FlutterTestSuite.java`
.
This makes sure the test is actually executed at run time.
test jar.
3.
Import your test class and add it to the
`@SuiteClasses`
annotation in
`FlutterTestSuite.java`
. This makes sure the test is actually executed at
run time.
4.
Write your test.
5.
Build and run with
`testing/run_tests.py [--type=java] [--java-filter=<test_class_name>]`
.
...
...
shell/platform/android/test/io/flutter/FlutterTestSuite.java
浏览文件 @
81f219c5
...
...
@@ -30,6 +30,7 @@ import io.flutter.external.FlutterLaunchTests;
import
io.flutter.plugin.common.StandardMessageCodecTest
;
import
io.flutter.plugin.common.StandardMethodCodecTest
;
import
io.flutter.plugin.editing.InputConnectionAdaptorTest
;
import
io.flutter.plugin.editing.ListenableEditingStateTest
;
import
io.flutter.plugin.editing.TextInputPluginTest
;
import
io.flutter.plugin.mouse.MouseCursorPluginTest
;
import
io.flutter.plugin.platform.PlatformPluginTest
;
...
...
@@ -70,6 +71,7 @@ import test.io.flutter.embedding.engine.PluginComponentTest;
FlutterViewTest
.
class
,
InputConnectionAdaptorTest
.
class
,
KeyEventChannelTest
.
class
,
ListenableEditingStateTest
.
class
,
LocalizationPluginTest
.
class
,
MouseCursorPluginTest
.
class
,
PlatformChannelTest
.
class
,
...
...
shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java
浏览文件 @
81f219c5
此差异已折叠。
点击以展开。
shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java
0 → 100644
浏览文件 @
81f219c5
package
io.flutter.plugin.editing
;
import
static
org
.
junit
.
Assert
.
assertEquals
;
import
static
org
.
junit
.
Assert
.
assertFalse
;
import
static
org
.
junit
.
Assert
.
assertTrue
;
import
static
org
.
mockito
.
Mockito
.
mock
;
import
android.text.Editable
;
import
android.text.Selection
;
import
android.view.View
;
import
android.view.inputmethod.BaseInputConnection
;
import
android.view.inputmethod.EditorInfo
;
import
io.flutter.embedding.android.AndroidKeyProcessor
;
import
io.flutter.embedding.engine.systemchannels.TextInputChannel
;
import
java.util.ArrayList
;
import
org.junit.Test
;
import
org.junit.runner.RunWith
;
import
org.robolectric.RobolectricTestRunner
;
import
org.robolectric.RuntimeEnvironment
;
import
org.robolectric.annotation.Config
;
@Config
(
manifest
=
Config
.
NONE
)
@RunWith
(
RobolectricTestRunner
.
class
)
public
class
ListenableEditingStateTest
{
private
BaseInputConnection
getTestInputConnection
(
View
view
,
Editable
mEditable
)
{
new
View
(
RuntimeEnvironment
.
application
);
return
new
BaseInputConnection
(
view
,
true
)
{
@Override
public
Editable
getEditable
()
{
return
mEditable
;
}
};
}
// -------- Start: Test BatchEditing -------
@Test
public
void
testBatchEditing
()
{
final
ListenableEditingState
editingState
=
new
ListenableEditingState
(
null
,
new
View
(
RuntimeEnvironment
.
application
));
final
Listener
listener
=
new
Listener
();
final
View
testView
=
new
View
(
RuntimeEnvironment
.
application
);
final
BaseInputConnection
inputConnection
=
getTestInputConnection
(
testView
,
editingState
);
editingState
.
addEditingStateListener
(
listener
);
editingState
.
replace
(
0
,
editingState
.
length
(),
"update"
);
assertTrue
(
listener
.
isCalled
());
assertTrue
(
listener
.
textChanged
);
assertFalse
(
listener
.
selectionChanged
);
assertFalse
(
listener
.
composingRegionChanged
);
assertEquals
(-
1
,
editingState
.
getSelectionStart
());
assertEquals
(-
1
,
editingState
.
getSelectionEnd
());
listener
.
reset
();
// Batch edit depth = 1.
editingState
.
beginBatchEdit
();
editingState
.
replace
(
0
,
editingState
.
length
(),
"update1"
);
assertFalse
(
listener
.
isCalled
());
// Batch edit depth = 2.
editingState
.
beginBatchEdit
();
editingState
.
replace
(
0
,
editingState
.
length
(),
"update2"
);
inputConnection
.
setComposingRegion
(
0
,
editingState
.
length
());
assertFalse
(
listener
.
isCalled
());
// Batch edit depth = 1.
editingState
.
endBatchEdit
();
assertFalse
(
listener
.
isCalled
());
// Batch edit depth = 2.
editingState
.
beginBatchEdit
();
assertFalse
(
listener
.
isCalled
());
inputConnection
.
setSelection
(
0
,
0
);
assertFalse
(
listener
.
isCalled
());
// Batch edit depth = 1.
editingState
.
endBatchEdit
();
assertFalse
(
listener
.
isCalled
());
// Remove composing region.
inputConnection
.
finishComposingText
();
// Batch edit depth = 0. Last endBatchEdit.
editingState
.
endBatchEdit
();
// Now notify the listener.
assertTrue
(
listener
.
isCalled
());
assertTrue
(
listener
.
textChanged
);
assertFalse
(
listener
.
composingRegionChanged
);
}
@Test
public
void
testBatchingEditing_callEndBeforeBegin
()
{
final
ListenableEditingState
editingState
=
new
ListenableEditingState
(
null
,
new
View
(
RuntimeEnvironment
.
application
));
final
Listener
listener
=
new
Listener
();
editingState
.
addEditingStateListener
(
listener
);
editingState
.
endBatchEdit
();
assertFalse
(
listener
.
isCalled
());
editingState
.
replace
(
0
,
editingState
.
length
(),
"text"
);
assertTrue
(
listener
.
isCalled
());
assertTrue
(
listener
.
textChanged
);
listener
.
reset
();
// Does not disrupt the followup events.
editingState
.
beginBatchEdit
();
editingState
.
replace
(
0
,
editingState
.
length
(),
"more text"
);
assertFalse
(
listener
.
isCalled
());
editingState
.
endBatchEdit
();
assertTrue
(
listener
.
isCalled
());
}
@Test
public
void
testBatchingEditing_addListenerDuringBatchEdit
()
{
final
ListenableEditingState
editingState
=
new
ListenableEditingState
(
null
,
new
View
(
RuntimeEnvironment
.
application
));
final
Listener
listener
=
new
Listener
();
editingState
.
beginBatchEdit
();
editingState
.
addEditingStateListener
(
listener
);
editingState
.
replace
(
0
,
editingState
.
length
(),
"update"
);
editingState
.
endBatchEdit
();
assertTrue
(
listener
.
isCalled
());
assertTrue
(
listener
.
textChanged
);
assertTrue
(
listener
.
selectionChanged
);
assertTrue
(
listener
.
composingRegionChanged
);
listener
.
reset
();
// Verifies the listener is officially added.
editingState
.
replace
(
0
,
editingState
.
length
(),
"more updates"
);
assertTrue
(
listener
.
isCalled
());
assertTrue
(
listener
.
textChanged
);
editingState
.
removeEditingStateListener
(
listener
);
listener
.
reset
();
// Now remove before endBatchEdit();
editingState
.
beginBatchEdit
();
editingState
.
addEditingStateListener
(
listener
);
editingState
.
replace
(
0
,
editingState
.
length
(),
"update"
);
editingState
.
removeEditingStateListener
(
listener
);
editingState
.
endBatchEdit
();
assertFalse
(
listener
.
isCalled
());
}
@Test
public
void
testBatchingEditing_removeListenerDuringBatchEdit
()
{
final
ListenableEditingState
editingState
=
new
ListenableEditingState
(
null
,
new
View
(
RuntimeEnvironment
.
application
));
final
Listener
listener
=
new
Listener
();
editingState
.
addEditingStateListener
(
listener
);
editingState
.
beginBatchEdit
();
editingState
.
replace
(
0
,
editingState
.
length
(),
"update"
);
editingState
.
removeEditingStateListener
(
listener
);
editingState
.
endBatchEdit
();
assertFalse
(
listener
.
isCalled
());
}
@Test
public
void
testBatchingEditing_listenerCallsReplaceWhenBatchEditEnds
()
{
final
ListenableEditingState
editingState
=
new
ListenableEditingState
(
null
,
new
View
(
RuntimeEnvironment
.
application
));
final
Listener
listener
=
new
Listener
()
{
@Override
public
void
didChangeEditingState
(
boolean
textChanged
,
boolean
selectionChanged
,
boolean
composingRegionChanged
)
{
super
.
didChangeEditingState
(
textChanged
,
selectionChanged
,
composingRegionChanged
);
editingState
.
replace
(
0
,
editingState
.
length
(),
"one does not simply replace the text in the listener"
);
}
};
editingState
.
addEditingStateListener
(
listener
);
editingState
.
beginBatchEdit
();
editingState
.
replace
(
0
,
editingState
.
length
(),
"update"
);
editingState
.
endBatchEdit
();
assertTrue
(
listener
.
isCalled
());
assertEquals
(
1
,
listener
.
timesCalled
);
assertEquals
(
"one does not simply replace the text in the listener"
,
editingState
.
toString
());
}
// -------- End: Test BatchEditing -------
@Test
public
void
testSetComposingRegion
()
{
final
ListenableEditingState
editingState
=
new
ListenableEditingState
(
null
,
new
View
(
RuntimeEnvironment
.
application
));
editingState
.
replace
(
0
,
editingState
.
length
(),
"text"
);
// (-1, -1) clears the composing region.
editingState
.
setComposingRange
(-
1
,
-
1
);
assertEquals
(-
1
,
editingState
.
getComposingStart
());
assertEquals
(-
1
,
editingState
.
getComposingEnd
());
editingState
.
setComposingRange
(-
1
,
5
);
assertEquals
(-
1
,
editingState
.
getComposingStart
());
assertEquals
(-
1
,
editingState
.
getComposingEnd
());
editingState
.
setComposingRange
(
2
,
3
);
assertEquals
(
2
,
editingState
.
getComposingStart
());
assertEquals
(
3
,
editingState
.
getComposingEnd
());
// Empty range is invalid. Clears composing region.
editingState
.
setComposingRange
(
1
,
1
);
assertEquals
(-
1
,
editingState
.
getComposingStart
());
assertEquals
(-
1
,
editingState
.
getComposingEnd
());
// Covers everything.
editingState
.
setComposingRange
(
0
,
editingState
.
length
());
assertEquals
(
0
,
editingState
.
getComposingStart
());
assertEquals
(
editingState
.
length
(),
editingState
.
getComposingEnd
());
}
// -------- Start: Test InputMethods actions -------
@Test
public
void
inputMethod_batchEditingBeginAndEnd
()
{
final
ArrayList
<
String
>
batchMarkers
=
new
ArrayList
<>();
final
ListenableEditingState
editingState
=
new
ListenableEditingState
(
null
,
new
View
(
RuntimeEnvironment
.
application
))
{
@Override
public
final
void
beginBatchEdit
()
{
super
.
beginBatchEdit
();
batchMarkers
.
add
(
"begin"
);
}
@Override
public
void
endBatchEdit
()
{
super
.
endBatchEdit
();
batchMarkers
.
add
(
"end"
);
}
};
final
Listener
listener
=
new
Listener
();
final
View
testView
=
new
View
(
RuntimeEnvironment
.
application
);
final
AndroidKeyProcessor
mockKeyProcessor
=
mock
(
AndroidKeyProcessor
.
class
);
final
InputConnectionAdaptor
inputConnection
=
new
InputConnectionAdaptor
(
testView
,
0
,
mock
(
TextInputChannel
.
class
),
mockKeyProcessor
,
editingState
,
new
EditorInfo
());
// Make sure begin/endBatchEdit is called on the Editable when the input method calls
// InputConnection#begin/endBatchEdit.
inputConnection
.
beginBatchEdit
();
assertEquals
(
1
,
batchMarkers
.
size
());
assertEquals
(
"begin"
,
batchMarkers
.
get
(
0
));
inputConnection
.
endBatchEdit
();
assertEquals
(
2
,
batchMarkers
.
size
());
assertEquals
(
"end"
,
batchMarkers
.
get
(
1
));
}
@Test
public
void
inputMethod_testSetSelection
()
{
final
ListenableEditingState
editingState
=
new
ListenableEditingState
(
null
,
new
View
(
RuntimeEnvironment
.
application
));
final
Listener
listener
=
new
Listener
();
final
View
testView
=
new
View
(
RuntimeEnvironment
.
application
);
final
AndroidKeyProcessor
mockKeyProcessor
=
mock
(
AndroidKeyProcessor
.
class
);
final
InputConnectionAdaptor
inputConnection
=
new
InputConnectionAdaptor
(
testView
,
0
,
mock
(
TextInputChannel
.
class
),
mockKeyProcessor
,
editingState
,
new
EditorInfo
());
editingState
.
replace
(
0
,
editingState
.
length
(),
"initial text"
);
editingState
.
addEditingStateListener
(
listener
);
inputConnection
.
setSelection
(
0
,
0
);
assertTrue
(
listener
.
isCalled
());
assertFalse
(
listener
.
textChanged
);
assertTrue
(
listener
.
selectionChanged
);
assertFalse
(
listener
.
composingRegionChanged
);
listener
.
reset
();
inputConnection
.
setSelection
(
5
,
5
);
assertTrue
(
listener
.
isCalled
());
assertFalse
(
listener
.
textChanged
);
assertTrue
(
listener
.
selectionChanged
);
assertFalse
(
listener
.
composingRegionChanged
);
}
@Test
public
void
inputMethod_testSetComposition
()
{
final
ListenableEditingState
editingState
=
new
ListenableEditingState
(
null
,
new
View
(
RuntimeEnvironment
.
application
));
final
Listener
listener
=
new
Listener
();
final
View
testView
=
new
View
(
RuntimeEnvironment
.
application
);
final
AndroidKeyProcessor
mockKeyProcessor
=
mock
(
AndroidKeyProcessor
.
class
);
final
InputConnectionAdaptor
inputConnection
=
new
InputConnectionAdaptor
(
testView
,
0
,
mock
(
TextInputChannel
.
class
),
mockKeyProcessor
,
editingState
,
new
EditorInfo
());
editingState
.
replace
(
0
,
editingState
.
length
(),
"initial text"
);
editingState
.
addEditingStateListener
(
listener
);
// setComposingRegion test.
inputConnection
.
setComposingRegion
(
1
,
3
);
assertTrue
(
listener
.
isCalled
());
assertFalse
(
listener
.
textChanged
);
assertFalse
(
listener
.
selectionChanged
);
assertTrue
(
listener
.
composingRegionChanged
);
Selection
.
setSelection
(
editingState
,
0
,
0
);
listener
.
reset
();
// setComposingText test: non-empty text, does not move cursor.
inputConnection
.
setComposingText
(
"composing"
,
-
1
);
assertTrue
(
listener
.
isCalled
());
assertTrue
(
listener
.
textChanged
);
assertFalse
(
listener
.
selectionChanged
);
assertTrue
(
listener
.
composingRegionChanged
);
listener
.
reset
();
// setComposingText test: non-empty text, moves cursor.
inputConnection
.
setComposingText
(
"composing2"
,
1
);
assertTrue
(
listener
.
isCalled
());
assertTrue
(
listener
.
textChanged
);
assertTrue
(
listener
.
selectionChanged
);
assertTrue
(
listener
.
composingRegionChanged
);
listener
.
reset
();
// setComposingText test: empty text.
inputConnection
.
setComposingText
(
""
,
1
);
assertTrue
(
listener
.
isCalled
());
assertTrue
(
listener
.
textChanged
);
assertTrue
(
listener
.
selectionChanged
);
assertTrue
(
listener
.
composingRegionChanged
);
// finishComposingText test.
inputConnection
.
setComposingText
(
"composing text"
,
1
);
listener
.
reset
();
inputConnection
.
finishComposingText
();
assertTrue
(
listener
.
isCalled
());
assertFalse
(
listener
.
textChanged
);
assertFalse
(
listener
.
selectionChanged
);
assertTrue
(
listener
.
composingRegionChanged
);
}
@Test
public
void
inputMethod_testCommitText
()
{
final
ListenableEditingState
editingState
=
new
ListenableEditingState
(
null
,
new
View
(
RuntimeEnvironment
.
application
));
final
Listener
listener
=
new
Listener
();
final
View
testView
=
new
View
(
RuntimeEnvironment
.
application
);
final
AndroidKeyProcessor
mockKeyProcessor
=
mock
(
AndroidKeyProcessor
.
class
);
final
InputConnectionAdaptor
inputConnection
=
new
InputConnectionAdaptor
(
testView
,
0
,
mock
(
TextInputChannel
.
class
),
mockKeyProcessor
,
editingState
,
new
EditorInfo
());
editingState
.
replace
(
0
,
editingState
.
length
(),
"initial text"
);
editingState
.
addEditingStateListener
(
listener
);
}
// -------- End: Test InputMethods actions -------
public
static
class
Listener
implements
ListenableEditingState
.
EditingStateWatcher
{
public
boolean
isCalled
()
{
return
timesCalled
>
0
;
}
int
timesCalled
=
0
;
boolean
textChanged
=
false
;
boolean
selectionChanged
=
false
;
boolean
composingRegionChanged
=
false
;
@Override
public
void
didChangeEditingState
(
boolean
textChanged
,
boolean
selectionChanged
,
boolean
composingRegionChanged
)
{
timesCalled
++;
this
.
textChanged
=
textChanged
;
this
.
selectionChanged
=
selectionChanged
;
this
.
composingRegionChanged
=
composingRegionChanged
;
}
public
void
reset
()
{
timesCalled
=
0
;
textChanged
=
false
;
selectionChanged
=
false
;
composingRegionChanged
=
false
;
}
}
}
shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
浏览文件 @
81f219c5
此差异已折叠。
点击以展开。
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录