Skip to content

Commit 2500441

Browse files
committed
Update
1 parent 46efff6 commit 2500441

6 files changed

Lines changed: 176 additions & 16 deletions

File tree

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import android.view.MenuItem
1414
import android.view.View
1515
import android.webkit.*
1616
import android.widget.Toast
17-
import androidx.activity.addCallback
1817
import androidx.appcompat.app.AlertDialog
1918
import androidx.appcompat.app.AppCompatActivity
2019
import androidx.appcompat.widget.Toolbar
@@ -65,13 +64,13 @@ class ActionPageOnline : AppCompatActivity() {
6564

6665
binding.krOnlineWebview.setLayerType(View.LAYER_TYPE_HARDWARE, null)
6766

68-
onBackPressedDispatcher.addCallback(this) {
67+
PredictiveBackHelper(this, binding.root) {
6968
if (binding.krOnlineWebview.canGoBack()) {
7069
binding.krOnlineWebview.goBack()
7170
} else {
7271
finish()
7372
}
74-
}
73+
}.register(onBackPressedDispatcher)
7574

7675
loadIntentData()
7776
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import com.omarea.common.ui.ProgressBarDialog
1212
import com.tool.tree.databinding.ActivityFileSelectorBinding
1313
import com.tool.tree.ui.AdapterFileSelector
1414
import java.io.File
15-
import androidx.activity.addCallback
1615
import com.google.android.material.snackbar.Snackbar;
1716

1817
class ActivityFileSelector : AppCompatActivity() {
@@ -49,11 +48,12 @@ class ActivityFileSelector : AppCompatActivity() {
4948
finish()
5049
}
5150

52-
onBackPressedDispatcher.addCallback(this) {
53-
if (adapterFileSelector?.goParent() == true) return@addCallback
54-
setResult(RESULT_CANCELED, Intent())
55-
finish()
56-
}
51+
PredictiveBackHelper(this, binding.root) {
52+
if (adapterFileSelector?.goParent() != true) {
53+
setResult(RESULT_CANCELED, Intent())
54+
finish()
55+
}
56+
}.register(onBackPressedDispatcher)
5757

5858
intent.extras?.run {
5959
if (containsKey("extension")) {

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import android.widget.ArrayAdapter
1111
import android.widget.CheckBox
1212
import android.widget.TextView
1313
import android.widget.Toast
14-
import androidx.activity.addCallback
1514
import androidx.appcompat.app.AppCompatActivity
1615
import androidx.appcompat.widget.ListPopupWindow
1716
import androidx.appcompat.widget.Toolbar
@@ -30,6 +29,7 @@ import com.omarea.krscript.ui.ParamsFileChooserRender
3029
import com.tool.tree.databinding.ActivityMainBinding
3130
import com.tool.tree.ui.MainPagerAdapter
3231
import com.tool.tree.ui.TabIconHelper
32+
import com.tool.tree.ui.FadeThroughPageTransformer
3333
import kotlinx.coroutines.Dispatchers
3434
import kotlinx.coroutines.isActive
3535
import kotlinx.coroutines.launch
@@ -65,21 +65,22 @@ class MainActivity : AppCompatActivity() {
6565
initAdapter()
6666
loadTabs()
6767

68-
onBackPressedDispatcher.addCallback(this) {
68+
PredictiveBackHelper(this, binding.root) {
6969
startService(Intent(this@MainActivity, WakeLockService::class.java).apply {
7070
action = WakeLockService.ACTION_END_WAKELOCK
7171
})
72-
// isEnabled = false
73-
// onBackPressedDispatcher.onBackPressed()
7472
finish()
75-
}
73+
}.register(onBackPressedDispatcher)
7674
}
7775

7876
private fun initAdapter() {
7977
if (!::adapter.isInitialized) {
8078
adapter = MainPagerAdapter(this)
8179
binding.viewPager.adapter = adapter
8280
binding.viewPager.offscreenPageLimit = 4
81+
// Dùng hiệu ứng "Fade Through" (mờ dần, không trượt) khi chuyển tab,
82+
// thay cho hiệu ứng slide mặc định kiểu AOSP.
83+
binding.viewPager.setPageTransformer(FadeThroughPageTransformer())
8384
}
8485
}
8586

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.tool.tree
2+
3+
import android.app.Activity
4+
import android.view.View
5+
import androidx.activity.BackEventCompat
6+
import androidx.activity.OnBackPressedCallback
7+
import androidx.activity.OnBackPressedDispatcher
8+
9+
/**
10+
* Hiệu ứng "vuốt để trở lại" (Predictive Back Gesture - Android 13+/API 33+) cho 1 Activity.
11+
*
12+
* Mặc định, dùng `onBackPressedDispatcher.addCallback(this) { ... }` (callback đơn giản)
13+
* sẽ CHẶN MẤT hiệu ứng preview: hệ thống chỉ gọi hàm xử lý DUY NHẤT một lần khi người dùng
14+
* đã thả tay xong cử chỉ, giống hệt kiểu Back cũ của AOSP (đóng màn hình đột ngột, không có
15+
* animation theo tiến trình kéo).
16+
*
17+
* Helper này đăng ký một callback "biết" theo dõi toàn bộ tiến trình cử chỉ:
18+
* - handleOnBackStarted / handleOnBackProgressed: được gọi liên tục trong lúc ngón tay đang
19+
* kéo từ cạnh màn hình vào giữa. Ta dùng progress (0f -> 1f) để scale nhẹ nội dung màn hình
20+
* hiện tại (100% -> minScale) và dịch nó theo đúng hướng cạnh đang vuốt (trái/phải), tạo
21+
* cảm giác "kéo dần ra để lộ màn trước" giống Android hiện đại.
22+
* - handleOnBackPressed: được gọi khi người dùng THẢ TAY và cử chỉ hoàn tất -> thực thi hành
23+
* động back thật sự ([onBack], ví dụ finish() hoặc quay lại 1 bước trong ứng dụng).
24+
* - handleOnBackCancelled: được gọi khi người dùng kéo dở rồi rút tay lại giữa chừng (không
25+
* hoàn tất cử chỉ) -> animate mượt nội dung trở lại đúng như ban đầu (100% scale, vị trí gốc).
26+
*
27+
* Trên các máy/API < 33 (không hỗ trợ predictive back ở tầng hệ thống), các hàm
28+
* handleOnBackStarted/Progressed/Cancelled đơn giản là không được gọi - hành vi tự động
29+
* "rớt" về giống như back thường (không animation), không cần code riêng xử lý.
30+
*/
31+
class PredictiveBackHelper(
32+
private val activity: Activity,
33+
private val contentView: View,
34+
// Tỉ lệ scale nhỏ nhất khi kéo hết cỡ (progress = 1f). Vd 0.9f = thu nhỏ còn 90%.
35+
private val minScale: Float = 0.9f,
36+
// Khoảng dịch chuyển ngang tối đa (px) khi kéo hết cỡ, tạo cảm giác bị "kéo" theo tay.
37+
private val maxTranslatePx: Float = 48f,
38+
// Hành động back thật sự khi cử chỉ hoàn tất (thường là finish(), hoặc logic back riêng
39+
// của từng màn hình như WebView.goBack(), thoát thư mục con...).
40+
private val onBack: () -> Unit
41+
) {
42+
fun register(dispatcher: OnBackPressedDispatcher) {
43+
dispatcher.addCallback(activity, object : OnBackPressedCallback(true) {
44+
45+
override fun handleOnBackStarted(backEvent: BackEventCompat) {
46+
contentView.pivotY = contentView.height / 2f
47+
}
48+
49+
override fun handleOnBackProgressed(backEvent: BackEventCompat) {
50+
val progress = backEvent.progress.coerceIn(0f, 1f)
51+
52+
val scale = 1f - (1f - minScale) * progress
53+
contentView.scaleX = scale
54+
contentView.scaleY = scale
55+
56+
// EDGE_LEFT (vuốt từ mép trái vào) -> nội dung dịch sang phải theo tay.
57+
// EDGE_RIGHT (vuốt từ mép phải vào) -> nội dung dịch sang trái theo tay.
58+
val direction = if (backEvent.swipeEdge == BackEventCompat.EDGE_LEFT) 1f else -1f
59+
contentView.translationX = direction * maxTranslatePx * progress
60+
}
61+
62+
override fun handleOnBackPressed() {
63+
onBack()
64+
// Nếu onBack() không kết thúc Activity (vd chỉ lùi 1 bước trong app, như
65+
// WebView.goBack() hoặc thoát thư mục con), khôi phục lại giao diện về trạng
66+
// thái ban đầu vì người dùng vẫn đang ở lại màn hình này.
67+
// Nếu Activity đang finish() thì bỏ qua reset để animation thu nhỏ tiếp tục
68+
// mượt mà cho tới khi màn hình biến mất, không bị "giật" về rồi mới đóng.
69+
if (!activity.isFinishing) {
70+
resetContentView(animated = true)
71+
}
72+
}
73+
74+
override fun handleOnBackCancelled() {
75+
resetContentView(animated = true)
76+
}
77+
78+
private fun resetContentView(animated: Boolean) {
79+
if (animated) {
80+
contentView.animate()
81+
.scaleX(1f)
82+
.scaleY(1f)
83+
.translationX(0f)
84+
.setDuration(180)
85+
.start()
86+
} else {
87+
contentView.scaleX = 1f
88+
contentView.scaleY = 1f
89+
contentView.translationX = 0f
90+
}
91+
}
92+
})
93+
}
94+
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import android.view.ViewGroup
1616
import android.widget.HorizontalScrollView
1717
import android.widget.ImageButton
1818
import android.widget.Toast
19-
import androidx.activity.addCallback
2019
import androidx.appcompat.app.AppCompatActivity
2120
import androidx.appcompat.widget.Toolbar
2221
import androidx.core.view.ViewCompat
@@ -113,7 +112,7 @@ class TextEditorActivity : AppCompatActivity() {
113112
toolbar.setNavigationOnClickListener { attemptClose() }
114113
setupUndoRedoButtons(toolbar)
115114

116-
onBackPressedDispatcher.addCallback(this) { attemptClose() }
115+
PredictiveBackHelper(this, binding.root) { attemptClose() }.register(onBackPressedDispatcher)
117116

118117
filePath = intent.getStringExtra(EXTRA_FILE) ?: ""
119118
if (filePath.isEmpty()) {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.tool.tree.ui
2+
3+
import android.view.View
4+
import androidx.viewpager2.widget.ViewPager2
5+
import kotlin.math.abs
6+
7+
/**
8+
* Hiệu ứng chuyển tab kiểu Material "Fade Through" thay cho hiệu ứng trượt (slide)
9+
* mặc định của ViewPager2/AOSP.
10+
*
11+
* Cách hoạt động:
12+
* - "Đóng băng" translationX (dựa theo `position * width`) để loại bỏ hoàn toàn việc
13+
* trang bị kéo trượt ngang theo ngón tay/animation mặc định - trang luôn nằm cố định
14+
* giữa màn hình.
15+
* - Trang ĐANG hiện (outgoing) sẽ mờ dần và biến mất ở NỬA ĐẦU quá trình chuyển
16+
* (progress 0 -> 0.5), đồng thời thu nhỏ nhẹ 100% -> 92%.
17+
* - Trang SẮP hiện (incoming) sẽ mờ dần hiện ra ở NỬA SAU quá trình chuyển
18+
* (progress 0.5 -> 1), đồng thời phóng to nhẹ 92% -> 100%.
19+
* - Kết quả: khi vuốt/bấm tab, không thấy 2 trang "trượt" cạnh nhau như AOSP, mà thấy
20+
* trang cũ mờ đi rồi trang mới mờ hiện lên đúng tại chỗ.
21+
*/
22+
class FadeThroughPageTransformer(
23+
// Tỉ lệ scale nhỏ nhất khi trang đang mờ đi/mờ đến (giống Material spec ~0.92).
24+
// Đặt 1f nếu chỉ muốn fade thuần, không muốn scale.
25+
private val minScale: Float = 0.92f
26+
) : ViewPager2.PageTransformer {
27+
28+
override fun transformPage(page: View, position: Float) {
29+
val width = page.width
30+
if (width == 0) return
31+
32+
page.apply {
33+
// Hủy hiệu ứng trượt ngang mặc định: giữ trang luôn ở giữa màn hình bất kể
34+
// vị trí cuộn hiện tại, để chỉ còn lại hiệu ứng mờ dần (fade) + scale.
35+
translationX = -position * width
36+
37+
when {
38+
// Trang đã cuộn ra ngoài hẳn hai bên -> ẩn hoàn toàn, tránh tốn vẽ thừa.
39+
position <= -1f || position >= 1f -> {
40+
alpha = 0f
41+
scaleX = minScale
42+
scaleY = minScale
43+
}
44+
45+
// Trang đang là trang "cũ" (đang dần biến mất khi vuốt sang trái, tức
46+
// position đi từ 0 -> -1). progress = tiến độ rời đi (0 -> 1).
47+
position <= 0f -> {
48+
val progress = -position // 0 -> 1
49+
alpha = (1f - (progress / 0.5f)).coerceIn(0f, 1f)
50+
val scale = 1f - (1f - minScale) * progress.coerceIn(0f, 1f)
51+
scaleX = scale
52+
scaleY = scale
53+
}
54+
55+
// Trang đang là trang "mới" (đang dần hiện ra khi vuốt tới, position đi
56+
// từ 1 -> 0). progress = tiến độ xuất hiện (0 -> 1).
57+
else -> {
58+
val progress = 1f - position // 0 -> 1
59+
alpha = (((progress - 0.5f) / 0.5f)).coerceIn(0f, 1f)
60+
val scale = minScale + (1f - minScale) * progress.coerceIn(0f, 1f)
61+
scaleX = scale
62+
scaleY = scale
63+
}
64+
}
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)