Skip to content

Commit 337df62

Browse files
committed
Update
1 parent 65ef804 commit 337df62

2 files changed

Lines changed: 92 additions & 32 deletions

File tree

app/src/main/java/com/tool/tree/TextEditorActivity.kt

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package com.tool.tree
33
import android.content.Context
44
import android.content.Intent
55
import android.os.Bundle
6+
import android.text.Editable
7+
import android.text.TextWatcher
68
import android.view.Menu
79
import android.view.MenuItem
810
import android.view.View
11+
import android.view.ViewGroup
912
import android.widget.HorizontalScrollView
1013
import android.widget.Toast
1114
import 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 {

app/src/main/res/layout/activity_text_editor.xml

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,20 @@
1313
android:fillViewport="true"
1414
android:scrollbarStyle="insideOverlay">
1515

16-
<HorizontalScrollView
17-
android:id="@+id/editor_scroll_horizontal"
16+
<EditText
17+
android:id="@+id/editor_content"
1818
android:layout_width="match_parent"
19-
android:layout_height="match_parent"
20-
android:fillViewport="true"
21-
android:scrollbarStyle="insideOverlay">
22-
23-
<EditText
24-
android:id="@+id/editor_content"
25-
android:layout_width="match_parent"
26-
android:layout_height="match_parent"
27-
android:background="@android:color/transparent"
28-
android:gravity="top|start"
29-
android:hint="@string/editor_hint_empty"
30-
android:inputType="textMultiLine|textNoSuggestions"
31-
android:imeOptions="flagNoExtractUi"
32-
android:padding="16dp"
33-
android:textColor="?android:attr/textColorPrimary"
34-
android:textColorHint="?android:attr/textColorSecondary"
35-
android:textSize="14sp"
36-
android:typeface="monospace" />
37-
38-
</HorizontalScrollView>
19+
android:layout_height="wrap_content"
20+
android:background="@android:color/transparent"
21+
android:gravity="top|start"
22+
android:hint="@string/editor_hint_empty"
23+
android:inputType="textMultiLine|textNoSuggestions"
24+
android:imeOptions="flagNoExtractUi"
25+
android:padding="16dp"
26+
android:textColor="?android:attr/textColorPrimary"
27+
android:textColorHint="?android:attr/textColorSecondary"
28+
android:textSize="14sp"
29+
android:typeface="monospace" />
3930

4031
</ScrollView>
4132

0 commit comments

Comments
 (0)