@@ -3,9 +3,12 @@ package com.tool.tree
33import android.content.Context
44import android.content.Intent
55import android.os.Bundle
6+ import android.text.Editable
7+ import android.text.TextWatcher
68import android.view.Menu
79import android.view.MenuItem
810import android.view.View
11+ import android.view.ViewGroup
912import android.widget.HorizontalScrollView
1013import android.widget.Toast
1114import androidx.activity.addCallback
@@ -77,6 +80,8 @@ class TextEditorActivity : AppCompatActivity() {
7780 private var savedContent: String = " "
7881 private var isNewFile = false
7982 private var isSaving = false
83+ private var noWrapContainer: HorizontalScrollView ? = null
84+ private var fabBaseMarginBottom = 0
8085 private val progressBarDialog by lazy { ProgressBarDialog (this ) }
8186
8287 override fun onCreate (savedInstanceState : Bundle ? ) {
@@ -111,33 +116,72 @@ class TextEditorActivity : AppCompatActivity() {
111116
112117 applyWrapState()
113118 setupFab()
119+ setupCursorAutoScroll()
114120 loadFileContent()
115121 }
116122
117123 /* *
118124 * App đang dùng chế độ edge-to-edge (decorFitsSystemWindows = false) nên hệ thống sẽ
119125 * không tự đẩy layout lên khi hiện bàn phím dù có khai báo windowSoftInputMode="adjustResize"
120126 * trong manifest — phải tự lắng nghe inset của bàn phím (IME) rồi đệm (padding) đáy vùng
121- * nội dung lên tương ứng, đồng thời ẩn FAB đi để không bị bàn phím che mất/đè lên.
127+ * nội dung lên tương ứng, đồng thời đẩy FAB lên theo để nó luôn nổi trên bàn phím thay vì
128+ * bị che khuất hoặc biến mất.
122129 */
123130 private fun setupKeyboardInsets () {
124131 val scrollContainer = binding.mainList
132+ val fabParams = binding.editorFabRun.layoutParams as ViewGroup .MarginLayoutParams
133+ fabBaseMarginBottom = fabParams.bottomMargin
134+
125135 ViewCompat .setOnApplyWindowInsetsListener(scrollContainer) { view, insets ->
126136 val imeInset = insets.getInsets(WindowInsetsCompat .Type .ime()).bottom
127137 val systemBarsInset = insets.getInsets(WindowInsetsCompat .Type .systemBars()).bottom
128138 val keyboardVisible = imeInset > systemBarsInset
129139
130140 view.setPadding(view.paddingLeft, view.paddingTop, view.paddingRight, maxOf(imeInset, systemBarsInset))
131- binding.editorFabRun.visibility = if (keyboardVisible) View .GONE else View .VISIBLE
141+
142+ val fabLp = binding.editorFabRun.layoutParams as ViewGroup .MarginLayoutParams
143+ fabLp.bottomMargin = fabBaseMarginBottom + imeInset
144+ binding.editorFabRun.layoutParams = fabLp
132145
133146 if (keyboardVisible) {
134- view.post { scrollContainer.fullScroll( View . FOCUS_DOWN ) }
147+ view.post { scrollToCursor( ) }
135148 }
136149
137150 insets
138151 }
139152 }
140153
154+ /* *
155+ * Đảm bảo con trỏ luôn được cuộn vào vùng nhìn thấy mỗi khi gõ chữ/xuống dòng, kể cả khi
156+ * bàn phím đang chiếm một phần màn hình.
157+ */
158+ private fun setupCursorAutoScroll () {
159+ binding.editorContent.addTextChangedListener(object : TextWatcher {
160+ override fun beforeTextChanged (s : CharSequence? , start : Int , count : Int , after : Int ) {}
161+ override fun onTextChanged (s : CharSequence? , start : Int , before : Int , count : Int ) {}
162+ override fun afterTextChanged (s : Editable ? ) {
163+ binding.editorContent.post { scrollToCursor() }
164+ }
165+ })
166+ }
167+
168+ private fun scrollToCursor () {
169+ val editText = binding.editorContent
170+ val layout = editText.layout ? : return
171+ val scrollView = binding.mainList
172+ val selection = editText.selectionEnd.coerceIn(0 , editText.text?.length ? : 0 )
173+ val line = layout.getLineForOffset(selection)
174+ val lineTop = layout.getLineTop(line) + editText.top + editText.paddingTop
175+ val lineBottom = layout.getLineBottom(line) + editText.top
176+ val visibleTop = scrollView.scrollY
177+ val visibleBottom = visibleTop + scrollView.height - scrollView.paddingBottom
178+
179+ when {
180+ lineBottom > visibleBottom -> scrollView.smoothScrollTo(0 , lineBottom - scrollView.height + scrollView.paddingBottom)
181+ lineTop < visibleTop -> scrollView.smoothScrollTo(0 , lineTop)
182+ }
183+ }
184+
141185 private fun resolveAbsolutePath (dir : String , file : String ): String {
142186 if (file.startsWith(" /" )) return file
143187 val base = dir.ifEmpty { FileWrite .getPrivateFileDir(applicationContext) }
@@ -191,27 +235,52 @@ class TextEditorActivity : AppCompatActivity() {
191235
192236 private fun applyWrapState () {
193237 val editText = binding.editorContent
194- val horizontalScroll : HorizontalScrollView = binding.editorScrollHorizontal
238+ val scrollView = binding.mainList
195239
196240 // Lưu lại vị trí con trỏ và nội dung hiện tại trước khi đổi chế độ
197241 val currentText = editText.text?.toString().orEmpty()
198242 val cursorStart = editText.selectionStart.coerceIn(0 , currentText.length)
199243 val cursorEnd = editText.selectionEnd.coerceIn(0 , currentText.length)
200244
245+ // Gỡ EditText ra khỏi cha hiện tại (có thể là ScrollView hoặc HorizontalScrollView)
246+ (editText.parent as ? ViewGroup )?.removeView(editText)
247+ noWrapContainer?.let { scrollView.removeView(it) }
248+
201249 editText.setHorizontallyScrolling(! wrapEnabled)
202250
203- val params = editText.layoutParams
204- params.width = if (wrapEnabled) android.view.ViewGroup .LayoutParams .MATCH_PARENT else android.view.ViewGroup .LayoutParams .WRAP_CONTENT
205- editText.layoutParams = params
251+ if (wrapEnabled) {
252+ // Ngắt dòng: EditText là con trực tiếp của ScrollView (dọc) -> chiều rộng bị giới hạn
253+ // theo màn hình nên văn bản thực sự được ngắt dòng, chiều cao tự do để cuộn dọc.
254+ editText.layoutParams = ViewGroup .LayoutParams (
255+ ViewGroup .LayoutParams .MATCH_PARENT ,
256+ ViewGroup .LayoutParams .WRAP_CONTENT
257+ )
258+ scrollView.addView(editText)
259+ } else {
260+ // Không ngắt dòng: bọc EditText trong một HorizontalScrollView -> chiều rộng không bị
261+ // giới hạn nên các dòng dài sẽ kéo dài ra và cuộn ngang được, còn HorizontalScrollView
262+ // đó vẫn là con trực tiếp của ScrollView (dọc) nên vẫn cuộn dọc bình thường.
263+ val hsv = noWrapContainer ? : HorizontalScrollView (this ).also {
264+ it.isFillViewport = false
265+ it.scrollBarStyle = View .SCROLLBARS_INSIDE_OVERLAY
266+ noWrapContainer = it
267+ }
268+ editText.layoutParams = ViewGroup .LayoutParams (
269+ ViewGroup .LayoutParams .WRAP_CONTENT ,
270+ ViewGroup .LayoutParams .WRAP_CONTENT
271+ )
272+ hsv.removeAllViews()
273+ hsv.addView(editText)
274+ hsv.layoutParams = ViewGroup .LayoutParams (ViewGroup .LayoutParams .MATCH_PARENT , ViewGroup .LayoutParams .WRAP_CONTENT )
275+ hsv.scrollTo(0 , 0 )
276+ scrollView.addView(hsv)
277+ }
206278
207279 // Chỉ đổi setHorizontallyScrolling()/layoutParams thôi thì TextView không tự dựng lại
208280 // Layout nội bộ theo chiều rộng mới (văn bản đã dàn trang trước đó bị giữ nguyên), nên
209281 // phải set lại text để buộc nó dựng lại layout rồi khôi phục vị trí con trỏ.
210282 editText.setText(currentText)
211283 editText.setSelection(cursorStart.coerceAtMost(currentText.length), cursorEnd.coerceAtMost(currentText.length))
212-
213- horizontalScroll.isHorizontalScrollBarEnabled = ! wrapEnabled
214- horizontalScroll.scrollTo(0 , 0 )
215284 }
216285
217286 private fun hasUnsavedChanges (): Boolean {
0 commit comments