時間は第二部の出版のために来たので、今日は私たちのコードエディタを開発し、それを強調する自動補完やエラーを追加し、またその理由についてお話していきます任意のコードエディタが
EditText
遅れることはありません。
さらに読む前に、最初の部分を読むことを強くお勧めします。
前書き
まず、最後の部分で中断したところを思い出してみましょう。バックグラウンドのテキストを解析し、その可視部分のみに色を付ける最適化された構文の強調表示と、追加された行番号(Androidの改行はありませんが、それでも)を書きました。
この部分では、コード補完とエラーの強調表示を追加します。
コード補完
まず、それがどのように機能するかを想像してみましょう:
- ユーザーが単語を書く
- 最初のN文字を入力すると、ウィンドウにヒントが表示されます
- ヒントをクリックすると、単語が自動的に「印刷」されます
- ヒントのあるウィンドウが閉じ、カーソルが単語の最後に移動します
- ユーザーがツールチップに表示された単語を自分で入力した場合、ヒントのあるウィンドウは自動的に閉じるはずです
何か見えませんか?Androidには、まったく同じロジックを持つコンポーネントが既にあります。
MultiAutoCompleteTextView
そのため、松葉杖をPopupWindow
私たちと一緒に書く必要はありません(これらはすでに作成されています)。
最初のステップは、クラスの親を変更することです。
class TextProcessor @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.autoCompleteTextViewStyle
) : MultiAutoCompleteTextView(context, attrs, defStyleAttr)
次に
ArrayAdapter
、見つかった結果を表示するように記述する必要があります。完全なアダプタコードは使用できません。実装の例はインターネットで確認できます。しかし、今はフィルタリングで停止します。
するために
ArrayAdapter
ヒントを表示する必要があるかを理解することができ、我々はメソッドをオーバーライドする必要がありますgetFilter
:
override fun getFilter(): Filter {
return object : Filter() {
private val suggestions = mutableListOf<String>()
override fun performFiltering(constraint: CharSequence?): FilterResults {
// ...
}
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
clear() //
addAll(suggestions)
notifyDataSetChanged()
}
}
}
そしてメソッドで、ユーザーが入力し始めた単語(変数に含まれている)に基づいて単語
performFiltering
のリストsuggestions
を入力しますconstraint
。
フィルタリングする前にどこでデータを取得しますか?
それはすべてあなた次第です。ある種のインタープリターを使用して有効なオプションのみを選択するか、ファイルを開いたときにテキスト全体をスキャンできます。例を簡単にするために、自動補完オプションの既製のリストを使用します。
private val staticSuggestions = mutableListOf(
"function",
"return",
"var",
"const",
"let",
"null"
...
)
...
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filterResults = FilterResults()
val input = constraint.toString()
suggestions.clear() //
for (suggestion in staticSuggestions) {
if (suggestion.startsWith(input, ignoreCase = true) &&
!suggestion.equals(input, ignoreCase = true)) {
suggestions.add(suggestion)
}
}
filterResults.values = suggestions
filterResults.count = suggestions.size
return filterResults
}
ここでのフィルタリングロジックはかなり原始的です。リスト全体を調べ、大文字と小文字を区別せずに文字列の先頭を比較します。
アダプタをインストールし、テキストを書きます-それは動作しません。どうしましたか?Googleの最初のリンクで、インストールを忘れたという答えに出くわし
Tokenizer
ます。
Tokenizerとは何ですか?
簡単に言えば、どの入力文字が入力された後、単語入力が完了したと見なすことができるか
Tokenizer
をMultiAutoCompleteTextView
理解するのに役立ちます。またCommaTokenizer
、単語をコンマに区切る形式の既成の実装もあり、この場合は適していません。
さて、
CommaTokenizer
私たちは満足していないので、私たちは自分で書きます:
カスタムトークナイザー
class SymbolsTokenizer : MultiAutoCompleteTextView.Tokenizer {
companion object {
private const val TOKEN = "!@#$%^&*()_+-={}|[]:;'<>/<.? \r\n\t"
}
override fun findTokenStart(text: CharSequence, cursor: Int): Int {
var i = cursor
while (i > 0 && !TOKEN.contains(text[i - 1])) {
i--
}
while (i < cursor && text[i] == ' ') {
i++
}
return i
}
override fun findTokenEnd(text: CharSequence, cursor: Int): Int {
var i = cursor
while (i < text.length) {
if (TOKEN.contains(text[i - 1])) {
return i
} else {
i++
}
}
return text.length
}
override fun terminateToken(text: CharSequence): CharSequence = text
}
それを理解しましょう:
TOKEN
-ある単語を別の単語から分離する文字列。メソッドfindTokenStart
でfindTokenEnd
は、これらの非常に分離した記号を探してテキストを調べます。このメソッドをterminateToken
使用すると、変更された結果を返すことができますが、必要ないため、テキストを変更せずに返します。
リストを表示する前に、2文字の入力遅延を追加することも好みます。
textProcessor.threshold = 2
インストール、実行、テキストの書き込み-機能します!しかし、何らかの理由でプロンプトのあるウィンドウが奇妙に動作します-全幅で表示され、高さが小さく、理論的にはカーソルの下に表示されるはずですが、どうすれば修正できますか?
視覚的な欠陥を修正する
ここからがおもしろいところです。APIを使用すると、ウィンドウのサイズだけでなく、その位置も変更できます。
まずはサイズを決めましょう。私の意見では、最も便利なオプションは画面の高さと幅の半分のウィンドウですが、サイズ
View
はキーボードの状態に応じて変化するため、メソッドでサイズを選択しますonSizeChanged
。
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
updateSyntaxHighlighting()
dropDownWidth = w * 1 / 2
dropDownHeight = h * 1 / 2
}
それは良く見えますが、それほどではありません。カーソルの下にウィンドウが表示され、編集中にウィンドウが移動するようにします。X
に沿って移動することですべてが非常に単純な場合-文字の先頭の座標を取得し、この値をに設定すると、高さの選択がより困難になります。 フォントの特性についてグーグル、あなたはこの投稿に出くわすことができます。著者が添付した画像は、垂直座標の計算に使用できるプロパティを明確に示しています。
dropDownHorizontalOffset
写真に基づいて、ベースラインは私たちが必要とするものです。オートコンプリートオプションのあるウィンドウが表示されるのはこのレベルです。
次に、テキストが次のように変化したときに呼び出すメソッドを記述します
onTextChanged
。
private fun onPopupChangePosition() {
val line = layout.getLineForOffset(selectionStart) //
val x = layout.getPrimaryHorizontal(selectionStart) //
val y = layout.getLineBaseline(line) // baseline
val offsetHorizontal = x + gutterWidth //
dropDownHorizontalOffset = offsetHorizontal.toInt()
val offsetVertical = y - scrollY // -scrollY ""
dropDownVerticalOffset = offsetVertical
}
彼らは何も忘れていないようです-Xオフセットは機能しますが、Yオフセットは正しく計算されません。これは
dropDownAnchor
、マークアップで指定しなかったためです。
android:dropDownAnchor="@id/toolbar"
指定することで
Toolbar
品質を、dropDownAnchor
私たちは、ドロップダウンリストが表示されることを知っているウィジェットせ下回ること。
これで、テキストの編集を開始すると、すべてが機能しますが、時間の経過とともに、ウィンドウがカーソルの下に収まらない場合は、醜いように見える大きなインデントで上にドラッグされます。松葉杖を書く時が来ました:
val offset = offsetVertical + dropDownHeight
if (offset < getVisibleHeight()) {
dropDownVerticalOffset = offsetVertical
} else {
dropDownVerticalOffset = offsetVertical - dropDownHeight
}
...
private fun getVisibleHeight(): Int {
val rect = Rect()
getWindowVisibleDisplayFrame(rect)
return rect.bottom - rect.top
}
合計が
offsetVertical + dropDownHeight
画面の表示可能な高さよりも小さい場合、インデントを変更する必要はありません。この場合、ウィンドウはカーソルの下に配置されているためです。しかし、それ以上の場合は、インデントから差し引きます。そのため、ウィジェット自体が追加する大きなインデントなしでカーソルの上dropDownHeight
に収まります。PSあなたはgifでキーボードが点滅しているのを見ることができます、そして正直に言うと、私はそれを修正する方法がわからないので、あなたが解決策を持っているなら、書いてください。
エラーの強調表示
エラーの強調表示を使用すると、コード内の構文エラーを直接検出できないため、見た目よりもすべてが単純になります。サードパーティのパーサーライブラリを使用します。私はJavaScriptのエディターを書いているので、長い間テストされ、現在もサポートされている人気のあるJavaScriptエンジンであるRhinoを選びました。
どのように解析しますか?
Rhinoの起動は非常に煩雑な操作であるため、(ハイライトで行ったように)各文字が入力された後にパーサーを実行することはまったく選択肢ではありません。この問題を解決するには、RxBindingライブラリを使用します。RxJavaをプロジェクトにドラッグしたくない場合は、同様のオプションを試すことができます。
オペレーター
debounce
は私たちが望むものを達成するのを助けてくれます。もしあなたが彼に慣れていないなら、私はこの記事を読むことを勧めます。
textProcessor.textChangeEvents()
.skipInitialValue()
.debounce(1500, TimeUnit.MILLISECONDS)
.filter { it.text.isNotEmpty() }
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
//
}
.disposeOnFragmentDestroyView()
次に、パーサーが返すモデルを作成しましょう。
data class ParseResult(val exception: RhinoException?)
次のロジックを使用することをお勧めし
exception
ますnull
。エラーが見つからない場合、エラーが発生します。それ以外の場合はRhinoException
、必要なすべての情報(行番号、エラーメッセージ、StackTraceなど)を含むオブジェクトを取得します。
まあ、実際には、解析自体:
// !
val context = Context.enter() // org.mozilla.javascript.Context
context.optimizationLevel = -1
context.maximumInterpreterStackDepth = 1
try {
val scope = context.initStandardObjects()
context.evaluateString(scope, sourceCode, fileName, 1, null)
return ParseResult(null)
} catch (e: RhinoException) {
return ParseResult(e)
} finally {
Context.exit()
}
理解:
ここで最も重要なのはメソッド
evaluateString
です。これにより、文字列として渡したコードを実行できますsourceCode
。fileName
ファイル名がで示されている-それはエラーで表示され、ユニットがカウントを開始する行番号で、最後の引数は、セキュリティドメインですが、我々が設定されて、私たちは、それを必要としませんnull
。
optimizationLevelおよびmaximumInterpreterStackDepth
1から9
optimizationLevel
までの値を
持つパラメーターを使用すると、特定のコードの「最適化」(データフロー分析、タイプフロー分析など)を有効にできます。これにより、単純な構文エラーチェックが非常に時間のかかる操作になり、私たちはそれを必要としません。
値を0にして使用すると、これらの「最適化」はすべて適用されませんが、私が正しく理解していれば、Rhinoは単純なエラーチェックに不要なリソースの一部を引き続き使用するため、適切ではありません。
負の値のみが残ります。- 1を指定することにより、「インタプリタ」モードをアクティブにします。これがまさに私たちが必要とするものです。ドキュメントには、これはRhinoのを実行するための最速かつ最も経済的な方法であると述べています。
パラメータを
maximumInterpreterStackDepth
使用すると、再帰呼び出しの数を制限できます。
このパラメーターを指定しないとどうなるかを想像してみましょう。
- ユーザーは次のコードを記述します。
function recurse() { recurse(); } recurse();
- Rhinoがコードを実行し、すぐにアプリケーションがでクラッシュし
OutOfMemoryError
ます。終わり。
エラーの表示
私が先に言ったように、できるだけ早く我々が得るとして
ParseResult
含むものをRhinoException
、私たちは、行番号を含む表示に必要なすべてのデータセットを、持っています-私たちはメソッドを呼び出す必要がありますlineNumber()
。StackOverflow
にコピーした赤い波線のスパンを書きましょう。コードはたくさんありますが、ロジックは単純です-異なる角度で2本の短い赤い線を描きます。
ErrorSpan.kt
class ErrorSpan(
private val lineWidth: Float = 1 * Resources.getSystem().displayMetrics.density + 0.5f,
private val waveSize: Float = 3 * Resources.getSystem().displayMetrics.density + 0.5f,
private val color: Int = Color.RED
) : LineBackgroundSpan {
override fun drawBackground(
canvas: Canvas,
paint: Paint,
left: Int,
right: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
lineNumber: Int
) {
val width = paint.measureText(text, start, end)
val linePaint = Paint(paint)
linePaint.color = color
linePaint.strokeWidth = lineWidth
val doubleWaveSize = waveSize * 2
var i = left.toFloat()
while (i < left + width) {
canvas.drawLine(i, bottom.toFloat(), i + waveSize, bottom - waveSize, linePaint)
canvas.drawLine(i + waveSize, bottom - waveSize, i + doubleWaveSize, bottom.toFloat(), linePaint)
i += doubleWaveSize
}
}
}
これで、問題のある行にスパンをインストールする方法を書くことができます:
fun setErrorLine(lineNumber: Int) {
if (lineNumber in 0 until lineCount) {
val lineStart = layout.getLineStart(lineNumber)
val lineEnd = layout.getLineEnd(lineNumber)
text.setSpan(ErrorSpan(), lineStart, lineEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
結果には遅延が伴うため、ユーザーは数行のコードを消去する時間
lineNumber
があり、無効になる可能性があることに注意してください。
したがって、それを取得しないようにするために、
IndexOutOfBoundsException
最初にチェックを追加します。それでは、おなじみのスキームに従って、文字列の最初と最後の文字を計算し、その後スパンを設定します。
主なことは、すでに設定されているスパンからテキストをクリアすることを忘れないことです
afterTextChanged
:
fun clearErrorSpans() {
val spans = text.getSpans<ErrorSpan>(0, text.length)
for (span in spans) {
text.removeSpan(span)
}
}
コードエディターが遅れるのはなぜですか?
2つの記事で、
EditText
およびを継承する優れたコードエディターをMultiAutoCompleteTextView
作成しましたが、大きなファイルを処理するときにパフォーマンスを誇ることはできません。
あなたは同じ開くとTextView.javaため9K +コードの行を、その後、任意のテキストエディタは我々が遅れると同じ原理に基づいて書かれました。
Q:なぜQuickEditは遅れないのですか?
A:内部で使用しているため、もも使用してい
EditText
ませんTextView
。
最近、のCustomView上のコードエディタは(人気を集めているこことそこにも、あるいは、こことそこに、たくさんあります)。歴史的に、TextViewにはコードエディターが必要としない冗長なロジックが多すぎます。最初に頭に浮かぶのは、オートフィル、絵文字、複合ドローアブル、クリック可能なリンクなどです。
私が正しく理解していれば、ライブラリの作成者は、これらすべてを単に取り除くだけでした。その結果、UIスレッドに大きな負荷をかけることなく、100万行のファイルを処理できるテキストエディターを手に入れました。 (私は部分的に間違っているかもしれませんが、私はソースをあまり理解していませんでした)
別のオプションがありますが、私の意見ではあまり魅力的ではありません-WebViewのコードエディター(あちこちにあります)、それらもたくさんあります)。WebViewのUIはネイティブのUIよりも見た目が悪く、パフォーマンスの点でCustomViewのエディターにも負けているため、私はそれらが好きではありません。
結論
コードエディターを作成してGoogle Playのトップに到達することがタスクの場合は、時間を無駄にせず、CustomViewで既成のライブラリーを使用してください。ユニークな体験をしたい場合は、ネイティブウィジェットを使用して自分ですべてを記述してください。
また、コードエディターのソースコードへのリンクをGitHubに残します。この2つの記事で説明した機能だけでなく、注意せずに残された他の多くの機能も見つかります。
感謝!