diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index 84de2d48..5537bb59 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -97,6 +97,7 @@ "部分设备无法正常初始化 MLKit Google OCR 的问题 (试修) _[`issue #8`](http://issues.autojs6.com/8#issuecomment-3117061768)_", "部分设备无法正常触发文件管理器功能按钮点击事件的问题 (试修) _[`issue #465`](http://issues.autojs6.com/465)_", "部分设备代码编辑器空行显示方框字符的问题 (试修)", + "代码编辑器软键盘可能遮挡文本内容的问题", "代码编辑器在只读模式下依然可以编辑代码内容的问题", "代码编辑器在只读模式下点击标题区域及部分菜单项导致应用崩溃的问题", "代码编辑器加载大文件可能导致应用崩溃的问题 (试修)", diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/EditActivity.kt b/app/src/main/java/org/autojs/autojs/ui/edit/EditActivity.kt index 440fa2b9..9a3f8fcc 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/EditActivity.kt +++ b/app/src/main/java/org/autojs/autojs/ui/edit/EditActivity.kt @@ -65,6 +65,8 @@ open class EditActivity : BaseActivity(), DelegateHost, PermissionRequestProxyAc if (mEditorView.onBackPressed()) { return } + mEditorView.cancelLargeFileLoading() + isEnabled = false onBackPressedDispatcher.onBackPressed() isEnabled = true diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt b/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt index 13e7f883..cc2cebed 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt +++ b/app/src/main/java/org/autojs/autojs/ui/edit/EditorView.kt @@ -8,6 +8,7 @@ import android.content.ContextWrapper import android.content.Intent import android.content.IntentFilter import android.database.Cursor +import android.graphics.Rect import android.net.Uri import android.os.Build import android.os.Bundle @@ -25,6 +26,9 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.core.view.GravityCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.FragmentActivity import com.afollestad.materialdialogs.MaterialDialog @@ -67,6 +71,7 @@ import org.autojs.autojs.ui.edit.debug.DebugBar import org.autojs.autojs.ui.edit.editor.CodeEditor import org.autojs.autojs.ui.edit.editor.CodeEditor.CheckedPatternSyntaxException import org.autojs.autojs.ui.edit.editor.JavaScriptHighlighter.MAX_HIGHLIGHT_CHARS +import org.autojs.autojs.ui.edit.editor.LayoutHelper import org.autojs.autojs.ui.edit.keyboard.FunctionsKeyboardHelper import org.autojs.autojs.ui.edit.keyboard.FunctionsKeyboardView import org.autojs.autojs.ui.edit.keyboard.FunctionsKeyboardView.ClickCallback @@ -107,7 +112,6 @@ import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference -import kotlin.text.set /** * Created by Stardust on Sep 28, 2017. @@ -241,12 +245,24 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag private val mShowLoadingBarRunnable = Runnable { if (!mLoadingBarRequested) return@Runnable - if (mLoadingBarContainer.visibility == VISIBLE) return@Runnable + if (mLoadingBarContainer.isVisible) return@Runnable mLoadingBarContainer.visibility = VISIBLE mLoadingBarShownAtMs = SystemClock.uptimeMillis() } + // IME bottom inset last applied to editor padding. + // zh-CN: 最近一次应用到编辑器 padding 的 IME 底部值. + private var mLastImeBottomInset = 0 + + // Reusable rect for cursor visibility requests. + // zh-CN: 光标可见性请求复用 Rect. + private val mTmpCursorRect = Rect() + + // Reusable rect for window visible area. + // zh-CN: 复用的窗口可见区域 Rect. + private val mTmpWindowVisibleRect = Rect() + constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) @@ -257,6 +273,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag setUpEditor() setUpInputMethodEnhancedBar() setUpFunctionsKeyboard() + setUpImeInsetsHandling() setMenuItemStatus(R.id.save, false) mDocsWebView.apply { webView.settings.displayZoomControls = true @@ -350,6 +367,112 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag } } + private fun findHostActivityOrNull(): Activity? { + var c = context + while (c !is Activity && c is ContextWrapper) { + c = c.baseContext + } + return c as? Activity + } + + private fun setUpImeInsetsHandling() { + // Handle IME insets to avoid keyboard covering the editor. + // zh-CN: 处理 IME inset, 避免软键盘遮挡编辑器内容. + ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets -> + val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) + + // Compute the real overlap between editor and the visible window area. + // zh-CN: 计算 editor 与窗口可见区域之间的真实重叠量. + val targetBottom = if (imeVisible) { + val activity = findHostActivityOrNull() + if (activity != null) { + activity.window.decorView.getWindowVisibleDisplayFrame(mTmpWindowVisibleRect) + + val loc = IntArray(2) + editor.getLocationOnScreen(loc) + val editorBottomOnScreen = loc[1] + editor.height + + (editorBottomOnScreen - mTmpWindowVisibleRect.bottom).coerceAtLeast(0) + } else { + 0 + } + } else 0 + + if (targetBottom != mLastImeBottomInset) { + mLastImeBottomInset = targetBottom + + // Apply bottom padding only when the editor is actually overlapped by IME. + // zh-CN: 仅当 editor 实际被 IME 遮挡时才应用底部 padding. + val v = editor + v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, targetBottom) + + // Only ensure cursor visible when IME becomes visible. + // zh-CN: 仅在 IME 变为可见时确保光标可见. + if (imeVisible) { + post { ensureCursorVisibleIfPossible() } + } + } + + // Do not consume insets. + // zh-CN: 不消费 insets. + insets + } + } + + private fun ensureCursorVisibleIfPossible() { + // Bring the caret into visible area when keyboard shows, without "scrolling back" on hide. + // zh-CN: 键盘弹起时将光标区域滚动到可见范围内, 键盘收起时不回滚. + val editText = editor.codeEditText + val textLen = editText.text?.length ?: 0 + if (textLen <= 0) return + + val layout = editText.layout ?: return + val sel = editText.selectionStart.coerceIn(0, textLen) + + val line = LayoutHelper.getLineOfChar(layout, sel).coerceAtLeast(0) + val lineTop = layout.getLineTop(line) + val lineBottom = layout.getLineBottom(line) + + // Compute a small rect around the current line/caret. + // zh-CN: 计算一个围绕当前行/光标的小矩形区域. + val x = runCatching { layout.getPrimaryHorizontal(sel).toInt() }.getOrElse { 0 } + val left = (x - editText.paddingLeft - 16).coerceAtLeast(0) + val right = left + 32 + + mTmpCursorRect.set( + /* left = */ left, + /* top = */ lineTop, + /* right = */ right, + /* bottom = */ lineBottom, + ) + + // Request rectangle visible inside scroll container. + // zh-CN: 请求在滚动容器内显示该矩形区域. + editText.requestRectangleOnScreen(mTmpCursorRect, true) + } + + private fun ensureEditorHasCursorAfterLoadIfNeeded() { + // Ensure there is a visible caret after loading, so jump operations can work. + // zh-CN: 加载完成后确保存在可见光标, 以使跳转操作可用. + val editText = editor.codeEditText + val len = editText.text?.length ?: 0 + if (len <= 0) return + + // If selection is invalid or collapsed at -1-like state, normalize. + // zh-CN: 若 selection 异常, 则归一化. + val safeSel = editText.selectionStart.coerceIn(0, len) + + // Request focus without forcing soft keyboard. + // zh-CN: 请求焦点但不强制弹出软键盘. + editText.requestFocus() + editText.isCursorVisible = true + editText.setSelection(safeSel) + + // Optionally ensure visible once. + // zh-CN: 可选地确保一次可见. + ensureCursorVisibleIfPossible() + } + private fun setLoadingBar(loading: Boolean) { if (loading) { mLoadingBarRequested = true @@ -430,7 +553,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag // Large file: show loading in enhance bar, allow viewing/scrolling (read-only). // zh-CN: 大文件: 在增强栏显示加载提示, 允许查看/滚动(只读). beginEditorLoadingUi(context.getString(R.string.text_loading_with_dots)) - return loadUriStreamed(uri, totalBytesOrNull = sizeOrNull) + return loadUriStreamed(uri) } return Observable @@ -508,7 +631,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag override fun onDetachedFromWindow() { // Cancel loading before unregistering receivers to avoid ANR on exit. // zh-CN: 退出前先取消加载, 避免返回退出时 ANR. - cancelLargeFileLoading("onDetachedFromWindow") + cancelLargeFileLoading() super.onDetachedFromWindow() context.unregisterReceiver(mOnRunFinishedReceiver) @@ -517,7 +640,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag // Cancel current large-file loading as soon as possible. // zh-CN: 尽快取消当前大文件加载. - private fun cancelLargeFileLoading(reason: String) { + fun cancelLargeFileLoading() { // Mark cancel first so both IO/UI can observe it. // zh-CN: 先标记取消, 让 IO/UI 两侧都能观察到. mLargeFileCancel.set(true) @@ -553,7 +676,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag } } - private fun loadUriStreamed(uri: Uri, totalBytesOrNull: Long?): Observable { + private fun loadUriStreamed(uri: Uri): Observable { val readBytes = AtomicLong(0L) // Reset cancel state for a new session. @@ -561,7 +684,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag mLargeFileCancel.set(false) return Observable - .create { emitter -> + .create { emitter -> val resolver = context.contentResolver // Emit an early placeholder so handleIntent() can proceed. @@ -620,16 +743,12 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag .let { upstream -> bridgeStreamToUiWithProgress( upstream = upstream, - totalBytesOrNull = totalBytesOrNull, - readBytes = readBytes, ) } } private fun bridgeStreamToUiWithProgress( upstream: Observable, - totalBytesOrNull: Long?, - readBytes: AtomicLong, ): Observable { val queue = ConcurrentLinkedQueue() val done = AtomicBoolean(false) @@ -669,7 +788,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag .observeOn(AndroidSchedulers.mainThread()) .doOnNext { if (started.compareAndSet(false, true)) { - startStreamingAppendWithProgress(queue, done, totalBytesOrNull, readBytes) + startStreamingAppendWithProgress(queue, done) } } } @@ -677,8 +796,6 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag private fun startStreamingAppendWithProgress( queue: ConcurrentLinkedQueue, done: AtomicBoolean, - totalBytesOrNull: Long?, - readBytes: AtomicLong, ) { val editText = editor.codeEditText @@ -733,6 +850,10 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag // zh-CN: 重要: 成功时必须结束加载 UI. endEditorLoadingUi() + // Ensure caret/focus after loading is done. + // zh-CN: 加载结束后确保光标/焦点. + post { ensureEditorHasCursorAfterLoadIfNeeded() } + mLargeFileFrameCallback = null return } @@ -812,127 +933,6 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag } } - private fun bridgeStreamToUi(upstream: Observable): Observable { - val queue = ConcurrentLinkedQueue() - val done = AtomicBoolean(false) - val started = AtomicBoolean(false) - - // Subscribe on IO side, push chunks into queue. - // zh-CN: 在 IO 侧订阅, 将分片推入队列. - upstream - .observeOn(Schedulers.io()) - .subscribe({ chunk -> - if (chunk.isNotEmpty()) { - queue.add(chunk) - } else { - // Ignore placeholder. - // zh-CN: 忽略占位分片. - } - }, { e -> - done.set(true) - post { editor.setProgress(false) } - e.printStackTrace() - }, { - done.set(true) - }) - - // Return a UI-side observable that triggers the append loop once. - // zh-CN: 返回 UI 侧 observable, 仅用于触发一次追加循环. - return Observable - .just("") - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { - if (started.compareAndSet(false, true)) { - startStreamingAppend(queue, done) - } - } - } - - private fun startStreamingAppend(queue: ConcurrentLinkedQueue, done: AtomicBoolean) { - val editText = editor.codeEditText - - // Large file streaming: disable all buttons while loading. - // zh-CN: 大文件流式加载: 加载期间禁用所有按钮. - mLargeFileMode = true - mEditorLoading = true - syncPrimaryMenuState() - - // Keep a non-blocking indicator during streaming. - // zh-CN: 流式加载期间保持一个不遮挡操作的提示. - editor.setProgress(true, interactive = true) - - editText.setLoadingText(true) - - // Keep undo/redo disabled in large file mode. - // zh-CN: 大文件模式下保持撤销/重做禁用. - editor.setRedoUndoEnabled(false) - - editText.setLoadingGutterDigits(7) - editText.setText("") - - val targetMs = 6L - var chunkSize = 64 * 1024 - val minChunkSize = 2 * 1024 - val maxChunkSize = 256 * 1024 - var lastAppendCostMs = 0L - - val choreographer = Choreographer.getInstance() - - val callback = object : Choreographer.FrameCallback { - override fun doFrame(frameTimeNanos: Long) { - val finished = done.get() && queue.isEmpty() - if (finished) { - // Finish: restore state and close progress. - // zh-CN: 完成: 恢复状态并关闭进度. - editText.setLoadingText(false) - - // Undo/redo stay disabled for large files. - // zh-CN: 大文件撤销/重做保持禁用. - editor.setRedoUndoEnabled(false) - editor.markUndoRedoBaselineAsUnchanged() - - editor.setProgress(false) - - mEditorLoading = false - syncPrimaryMenuState() - - val len = editText.text?.length ?: 0 - if (len in 1..MAX_HIGHLIGHT_CHARS) { - post { editor.refreshHighlightTokensIfAllowed() } - } - return - } - - val sb = StringBuilder(chunkSize.coerceAtLeast(1024)) - while (sb.length < chunkSize) { - val s = queue.poll() ?: break - sb.append(s) - } - - if (sb.isNotEmpty()) { - val t0 = SystemClock.uptimeMillis() - editText.beginBatchEdit() - try { - editText.text?.append(sb) - } finally { - editText.endBatchEdit() - } - lastAppendCostMs = (SystemClock.uptimeMillis() - t0).coerceAtLeast(0L) - - chunkSize = when { - lastAppendCostMs > targetMs * 2 -> (chunkSize / 2).coerceAtLeast(minChunkSize) - lastAppendCostMs < targetMs / 2 -> (chunkSize * 2).coerceAtMost(maxChunkSize) - else -> chunkSize - } - } - - choreographer.postFrameCallback(this) - } - } - - choreographer.postFrameCallback(callback) - } - private fun setInitialText(text: String) { // NOTE: This method should not manage loading UI lifecycle anymore. // zh-CN: 注意: 此方法不再管理加载 UI 的生命周期. @@ -971,6 +971,10 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag editText.setLoadingText(false) } + // Ensure caret/focus after fast load. + // zh-CN: 快速加载完成后确保光标/焦点. + post { ensureEditorHasCursorAfterLoadIfNeeded() } + // Re-highlight once after fast load, but only when size is within highlighter limit. // zh-CN: 快速加载完成后主动触发一次高亮, 但仅在文本大小不超过高亮上限时执行. if (text.length <= MAX_HIGHLIGHT_CHARS) { @@ -1073,6 +1077,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag // Adapt chunk size based on how many chunks we managed to append in this frame. // zh-CN: 根据本帧完成的分片数量自适应调整 chunkSize. + @Suppress("AssignedValueIsNeverRead") chunkSize = when { appendedChunks <= 1 -> (chunkSize / 2).coerceAtLeast(minChunkSize) appendedChunks >= 4 -> (chunkSize * 2).coerceAtMost(maxChunkSize) @@ -1553,7 +1558,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag } private fun buildSafeFileNameForSaveAs(baseName: String, ext: String): String { - val raw = if (baseName.isBlank()) "untitled" else baseName + val raw = baseName.ifBlank { "untitled" } val sanitized = raw.replace(Regex("""[\\/:*?"<>|]"""), "_") val niceExt = ext.trim().trimStart('.') return if (niceExt.isBlank()) sanitized else "$sanitized.$niceExt" @@ -1929,7 +1934,7 @@ class EditorView : LinearLayout, OnHintClickListener, ClickCallback, ToolbarFrag fun destroy() { // Cancel loading before destroying editor resources. // zh-CN: 销毁前先取消加载, 再销毁编辑器相关资源. - cancelLargeFileLoading("destroy") + cancelLargeFileLoading() editor.destroy() mAutoCompletion?.shutdown() diff --git a/version.properties b/version.properties index 504af6ee..6ba4b167 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Sun Feb 08 13:21:45 CST 2026 -BUILD_TIME=1770528105646 +#Sun Feb 08 15:05:14 CST 2026 +BUILD_TIME=1770534314495 COMPILE_SDK_VERSION=36 IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_NDK_VERSION=26.1.10909125 @@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 TARGET_SDK_VERSION=36 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=3713 +VERSION_BUILD=3714 VERSION_NAME=6.7.0 Alpha19 VSCODE_EXT_REQUIRED_VERSION=1.0.13