@@ -30,6 +30,7 @@ import com.omarea.krscript.model.RunnableNode
3030import com.omarea.krscript.model.ShellHandlerBase
3131import java.lang.ref.WeakReference
3232import com.tool.tree.AnsiColorParser
33+ import java.util.concurrent.atomic.AtomicBoolean
3334
3435class DialogLogFragment : DialogFragment () {
3536
@@ -229,6 +230,15 @@ class DialogLogFragment : DialogFragment() {
229230 private var lineStart = 0
230231 private var pendingOverwrite = false
231232
233+ // Độ dài logBuffer đã được đẩy lên UI (chỉ đọc/ghi trong synchronized(logBuffer))
234+ private var uiAppliedLength = 0
235+ // true nếu lần vẽ UI tiếp theo cần set lại toàn bộ text (do \r ghi đè hoặc log bị cắt tỉa)
236+ private var needFullRefresh = false
237+ // Đảm bảo nhiều lần cập nhật log liên tiếp chỉ gộp lại thành 1 lần vẽ UI / 1 lần cuộn,
238+ // tránh spam Runnable + fullScroll() lên Main thread khi shell xả log dồn dập -> đây là
239+ // nguyên nhân chính gây giật/lag khi log in ra nhanh.
240+ private val pendingUiUpdate = AtomicBoolean (false )
241+
232242 init {
233243 // Không set trực tiếp editable ban đầu nữa, quản lý qua append tĩnh cho mượt
234244 logView?.text = " "
@@ -293,6 +303,8 @@ class DialogLogFragment : DialogFragment() {
293303 lineCount = 0
294304 lineStart = 0
295305 pendingOverwrite = false
306+ uiAppliedLength = 0
307+ needFullRefresh = false
296308 logBuffer.clear()
297309 }
298310 logViewRef.get()?.post {
@@ -375,10 +387,6 @@ class DialogLogFragment : DialogFragment() {
375387 */
376388 private fun dispatchLogUpdate (formattedText : CharSequence ) {
377389 val logView = logViewRef.get() ? : return
378-
379- // Biến cục bộ để xác định xem dòng này có yêu cầu ghi đè hay không (\r)
380- var isOverwriteAction = false
381- val snapshotToDisplay: CharSequence
382390
383391 synchronized(logBuffer) {
384392 var i = 0
@@ -395,7 +403,7 @@ class DialogLogFragment : DialogFragment() {
395403 logBuffer.delete(lineStart, logBuffer.length)
396404 }
397405 pendingOverwrite = false
398- isOverwriteAction = true
406+ needFullRefresh = true
399407 }
400408 logBuffer.append(segment)
401409 }
@@ -412,7 +420,7 @@ class DialogLogFragment : DialogFragment() {
412420 if (pendingOverwrite && lineStart in 0 .. logBuffer.length) {
413421 logBuffer.delete(lineStart, logBuffer.length)
414422 pendingOverwrite = false
415- isOverwriteAction = true
423+ needFullRefresh = true
416424 }
417425 logBuffer.append(' \n ' )
418426 lineCount++
@@ -443,39 +451,60 @@ class DialogLogFragment : DialogFragment() {
443451 logBuffer.delete(0 , deleteEndIndex)
444452 lineCount = 5000
445453 lineStart = (lineStart - deleteEndIndex).coerceAtLeast(0 )
446- isOverwriteAction = true // Cần reload toàn bộ vì cấu trúc buffer đã dịch chuyển
454+ uiAppliedLength = (uiAppliedLength - deleteEndIndex).coerceAtLeast(0 )
455+ needFullRefresh = true // Cần reload toàn bộ vì cấu trúc buffer đã dịch chuyển
447456 }
448457 }
458+ }
449459
450- // Chụp lại trạng thái text hiện tại để ném lên UI hiển thị
451- snapshotToDisplay = SpannableStringBuilder (logBuffer)
460+ // GỘP CẬP NHẬT UI: nếu đã có 1 lần vẽ đang chờ chạy trên Main thread thì không post thêm.
461+ // Khi Main thread rảnh và chạy tới, flushToUi() sẽ tự đọc trạng thái MỚI NHẤT của logBuffer,
462+ // nên nhiều dòng log đến dồn dập chỉ gây ra 1 lần setText/append + 1 lần cuộn, thay vì
463+ // một Runnable + một fullScroll() cho từng dòng -> đây là nguyên nhân chính gây giật.
464+ if (pendingUiUpdate.compareAndSet(false , true )) {
465+ logView.post {
466+ pendingUiUpdate.set(false )
467+ flushToUi(logView)
468+ }
452469 }
470+ }
453471
454- // ĐẨY LÊN UI THREAD ĐỂ HIỂN THỊ
455- logView.post {
456- if (isOverwriteAction) {
457- // Nếu có lệnh xóa dòng cũ đè dòng mới (\r) hoặc vừa bị cắt tỉa log > 5000 dòng, bắt buộc phải đặt lại text
458- logView.text = snapshotToDisplay
459- } else {
460- // NẾU LÀ GHI TIẾP (APPEND): Đây là chìa khóa giúp log mượt!
461- // Thay vì nạp lại toàn bộ 5000 dòng, chỉ truyền phần text mới tinh vào.
462- val oldLen = logView.text.length
463- if (snapshotToDisplay.length > oldLen) {
464- val newChunk = snapshotToDisplay.subSequence(oldLen, snapshotToDisplay.length)
465- logView.append(newChunk)
466- }
472+ /* *
473+ * Vẽ lên UI. Bình thường chỉ append phần text MỚI (không copy lại toàn bộ buffer),
474+ * chỉ khi có \r ghi đè dòng hoặc log vừa bị cắt tỉa (>5000 dòng) mới cần set lại toàn bộ.
475+ */
476+ private fun flushToUi (logView : TextView ) {
477+ val chunk: CharSequence?
478+ val doFullRefresh: Boolean
479+
480+ synchronized(logBuffer) {
481+ doFullRefresh = needFullRefresh
482+ needFullRefresh = false
483+ chunk = when {
484+ doFullRefresh -> SpannableStringBuilder (logBuffer)
485+ logBuffer.length > uiAppliedLength -> logBuffer.subSequence(uiAppliedLength, logBuffer.length)
486+ else -> null
467487 }
488+ uiAppliedLength = logBuffer.length
489+ }
468490
469- // Tối ưu cuộn ScrollView xuống cuối
470- (logView.parent as ? ScrollView )?.let { scrollView ->
471- scrollView.fullScroll(ScrollView .FOCUS_DOWN )
472-
473- // Giữ focus thông minh không cần lồng post{} nhiều tầng
474- val inputRow = inputRowRef.get()
475- val input = shellInputRef.get()
476- if (inputRow != null && inputRow.visibility == View .VISIBLE && input != null ) {
477- if (! input.isFocused) input.requestFocus()
478- }
491+ if (chunk == null ) return
492+
493+ if (doFullRefresh) {
494+ logView.text = chunk
495+ } else {
496+ logView.append(chunk)
497+ }
498+
499+ // Tối ưu cuộn ScrollView xuống cuối
500+ (logView.parent as ? ScrollView )?.let { scrollView ->
501+ scrollView.fullScroll(ScrollView .FOCUS_DOWN )
502+
503+ // Giữ focus thông minh không cần lồng post{} nhiều tầng
504+ val inputRow = inputRowRef.get()
505+ val input = shellInputRef.get()
506+ if (inputRow != null && inputRow.visibility == View .VISIBLE && input != null ) {
507+ if (! input.isFocused) input.requestFocus()
479508 }
480509 }
481510 }
0 commit comments