6.7.0 - Alpha19 - 主页抽屉添加开关分隔线以增强视觉体验

This commit is contained in:
SuperMonster003
2026-02-12 17:52:57 +08:00
parent 0cd4435d2f
commit 5240084b00
6 changed files with 316 additions and 22 deletions

View File

@@ -6,6 +6,7 @@ import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
@@ -726,7 +727,6 @@ open class DrawerFragment : Fragment() {
drawerStatsDisposables.add(historyDisposable)
}
private fun configureDrawerWidth() {
val screenWidthDp = resources.configuration.screenWidthDp
val targetWidthPx = 288f.coerceIn(
@@ -741,6 +741,40 @@ open class DrawerFragment : Fragment() {
binding.pluginCenter.setOnClickListener { PluginCenterActivity.startActivity(it.context) }
binding.restart.setOnClickListener { restart(mActivity, mActivity::beforeExit) }
binding.exit.setOnClickListener { exit(mActivity, mActivity::beforeExit) }
// Consume touches on drawer blank area to prevent "press state" leaking to RecyclerView items.
// zh-CN: 消费抽屉空白区域触摸, 防止按压状态影响 RecyclerView item 的 ripple/pressed 表现.
binding.drawerMenuContainer.setOnTouchListener { v, ev ->
val rv = binding.drawerMenu
val buttonsRow = binding.settings.parent as? View
fun hitTest(target: View?): Boolean {
if (target == null) return false
val loc = IntArray(2)
target.getLocationOnScreen(loc)
val left = loc[0]
val top = loc[1]
val right = left + target.width
val bottom = top + target.height
val x = ev.rawX.toInt()
val y = ev.rawY.toInt()
return x in left until right && y in top until bottom
}
val onRecycler = hitTest(rv)
val onButtons = hitTest(buttonsRow)
if (onRecycler || onButtons) {
false
} else {
// Keep container from staying pressed.
// zh-CN: 避免容器保持 pressed 状态.
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
v.isPressed = false
}
true
}
}
}
override fun onResume() {

View File

@@ -7,6 +7,7 @@ import androidx.core.view.isVisible
import me.zhanghai.android.materialprogressbar.MaterialProgressBar
import org.autojs.autojs.ui.widget.BindableViewHolder
import org.autojs.autojs.ui.widget.PrefSwitch
import org.autojs.autojs.util.ViewUtils.installFullRowRippleForwarder
import org.autojs.autojs6.databinding.DrawerMenuItemBinding
/**
@@ -17,6 +18,7 @@ import org.autojs.autojs6.databinding.DrawerMenuItemBinding
class DrawerMenuItemViewHolder(itemView: View) : BindableViewHolder<DrawerMenuItem?>(itemView) {
private val mSwitchCompat: PrefSwitch
private val mSwitchDivider: View
private val mProgressBar: MaterialProgressBar
private val mIcon: ImageView
private val mTitle: TextView
@@ -30,6 +32,7 @@ class DrawerMenuItemViewHolder(itemView: View) : BindableViewHolder<DrawerMenuIt
init {
val binding = DrawerMenuItemBinding.bind(itemView)
mSwitchCompat = binding.sw
mSwitchDivider = binding.switchDivider
mProgressBar = binding.progressBar
mIcon = binding.icon
mTitle = binding.title
@@ -38,6 +41,13 @@ class DrawerMenuItemViewHolder(itemView: View) : BindableViewHolder<DrawerMenuIt
onClick()
}
val iconContainer = binding.iconContainer
val titleContainer = binding.titleContainer
// Install full-row ripple forwarding once.
// zh-CN: 安装整行 Ripple 转发, 通常只需安装一次.
installFullRowRippleForwarder(itemView, iconContainer, titleContainer)
val toggleAction = {
if (mSwitchCompat.isVisible) {
mSwitchCompat.toggle()
@@ -46,11 +56,11 @@ class DrawerMenuItemViewHolder(itemView: View) : BindableViewHolder<DrawerMenuIt
}
}
binding.iconContainer.setOnClickListener {
iconContainer.setOnClickListener {
toggleAction()
}
binding.iconContainer.setOnLongClickListener {
iconContainer.setOnLongClickListener {
when (val menuItem = mDrawerMenuItem) {
is DrawerMenuToggleableItem -> {
menuItem.launchManagerIfPossible()
@@ -59,7 +69,7 @@ class DrawerMenuItemViewHolder(itemView: View) : BindableViewHolder<DrawerMenuIt
}
}
binding.titleContainer.setOnClickListener {
titleContainer.setOnClickListener {
when (val menuItem = mDrawerMenuItem) {
is DrawerMenuToggleableItem -> {
menuItem.onTitleContainerClick()
@@ -68,7 +78,7 @@ class DrawerMenuItemViewHolder(itemView: View) : BindableViewHolder<DrawerMenuIt
}
}
binding.titleContainer.setOnLongClickListener {
titleContainer.setOnLongClickListener {
when (val menuItem = mDrawerMenuItem) {
is DrawerMenuToggleableItem -> {
menuItem.launchManagerIfPossible()
@@ -98,9 +108,11 @@ class DrawerMenuItemViewHolder(itemView: View) : BindableViewHolder<DrawerMenuIt
private fun setSwitch(item: DrawerMenuItem) {
if (!item.isSwitchEnabled) {
mSwitchCompat.visibility = View.GONE
mSwitchDivider.visibility = View.GONE
return
}
mSwitchCompat.visibility = View.VISIBLE
mSwitchDivider.visibility = View.VISIBLE
val prefKey = item.prefKey
if (prefKey == DrawerMenuItem.DEFAULT_PREFERENCE_KEY) {
mSwitchCompat.setChecked(item.isChecked, false)

View File

@@ -32,8 +32,10 @@ import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.Gravity
import android.view.Menu
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewTreeObserver
@@ -72,6 +74,7 @@ import org.autojs.autojs.runtime.ScriptRuntime
import org.autojs.autojs.theme.ThemeColorManager
import org.autojs.autojs.util.StringUtils.key
import org.autojs.autojs6.R
import kotlin.math.abs
import kotlin.math.floor
import kotlin.math.min
import kotlin.math.roundToInt
@@ -388,7 +391,7 @@ object ViewUtils {
* - 以 Type.navigationBars() 为主来源.
* - ignoreVisibility=true 时使用 "忽略可见性" 的 insets, 即使系统栏被隐藏也尽量返回稳定厚度.
* - ignoreVisibility=false 时使用 "可见" 的 insets, 反映当前可见状态.
* 2. computed(显示差值法), 可选.
* 2. computed (显示差值法), 可选.
* - 仅在 withComputed=true 且 pre-Android R 或 window==null 时启用.
* - 通过 real metrics 与 usable metrics 的差值计算系统 UI insets, 再推导导航栏厚度.
* 3. 内部 dimen, 可选.
@@ -846,6 +849,7 @@ object ViewUtils {
// zh-CN:
// 如果 view 未 attach 时调用 requestApplyInsets() 会被丢弃.
// 需要等 attach 后再请求一次, 才能稳定触发 insets 分发.
@Suppress("DEPRECATION")
private fun View.requestApplyInsetsWhenAttached() {
if (ViewCompat.isAttachedToWindow(this)) {
ViewCompat.requestApplyInsets(this)
@@ -1405,6 +1409,239 @@ object ViewUtils {
}
}
/**
* Install a ripple forwarder that makes full-row background react to touches on specific children,
* while preserving RecyclerView-like delayed pressed behavior.
* zh-CN:
* 安装 Ripple 转发器, 让整行背景响应指定子视图的触摸,
* 同时尽量模拟 RecyclerView 的延迟 pressed 行为.
*/
fun installFullRowRippleForwarder(
parent: View,
vararg touchSources: View,
pressDelayMillis: Int = ViewConfiguration.getTapTimeout(),
) {
// Ensure the parent can show stateful background/foreground.
// zh-CN: 确保父视图可以显示带状态的 background/foreground.
parent.isClickable = true
parent.isFocusable = true
val vc = ViewConfiguration.get(parent.context)
// Movement threshold to decide "scrolling" vs "tapping".
// zh-CN: 用于区分 "滚动" 与 "点击" 的移动阈值.
val touchSlop = vc.scaledTouchSlop
// Duration used by framework to show a short pressed state for quick taps.
// zh-CN: 系统用于 "快速点击" 时短暂显示 pressed 的持续时间.
val pressedStateDuration = ViewConfiguration.getPressedStateDuration()
// Reusable objects to avoid allocations in hot paths.
// zh-CN: 复用对象, 避免高频路径分配.
val parentLoc = IntArray(2)
val childHitRect = Rect()
touchSources.forEach { child ->
child.setOnTouchListener(object : View.OnTouchListener {
// Track one gesture stream.
// zh-CN: 跟踪一次手势流.
private var activePointerId: Int = MotionEvent.INVALID_POINTER_ID
// Down positions in raw (screen) coordinates.
// zh-CN: DOWN 时的 raw (屏幕) 坐标.
private var downRawX: Float = 0f
private var downRawY: Float = 0f
// Last known raw coordinates for hotspot update.
// zh-CN: 用于更新 hotspot 的最近 raw 坐标.
private var lastRawX: Float = 0f
private var lastRawY: Float = 0f
// Whether we have applied parent pressed state.
// zh-CN: 是否已应用父视图 pressed 状态.
private var pressedApplied: Boolean = false
// Whether we are still waiting to apply pressed (pre-pressed).
// zh-CN: 是否仍处于等待 pressed 的 pre-pressed 状态.
private var prePressed: Boolean = false
// Runnable to apply pressed after delay.
// zh-CN: 延迟触发 pressed 的 Runnable.
private val applyPressedRunnable = Runnable {
if (!prePressed) {
return@Runnable
}
if (!isInsideViewWithSlop(child, lastRawX, lastRawY, touchSlop, childHitRect)) {
prePressed = false
return@Runnable
}
applyPressedNow()
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
activePointerId = event.getPointerId(0)
downRawX = event.rawX
downRawY = event.rawY
lastRawX = event.rawX
lastRawY = event.rawY
pressedApplied = false
prePressed = true
// Delay pressed to mimic scrolling container behavior.
// zh-CN: 延迟 pressed, 模拟可滚动容器的行为.
parent.removeCallbacks(applyPressedRunnable)
parent.postDelayed(applyPressedRunnable, pressDelayMillis.toLong())
}
MotionEvent.ACTION_MOVE -> {
val idx = event.findPointerIndex(activePointerId)
if (idx < 0) {
return false
}
lastRawX = event.rawX
lastRawY = event.rawY
// If moved too much, treat as scroll and cancel.
// zh-CN: 如果移动过大, 视为滚动并取消.
val movedTooMuch =
abs(lastRawX - downRawX) > touchSlop || abs(lastRawY - downRawY) > touchSlop
if (movedTooMuch) {
cancelPressed()
return false
}
// If pressed is already applied, keep hotspot in sync.
// zh-CN: 若 pressed 已应用, 持续同步 hotspot.
if (pressedApplied) {
updateHotspot(lastRawX, lastRawY)
}
}
MotionEvent.ACTION_UP -> {
val idx = event.findPointerIndex(activePointerId)
if (idx < 0) {
cancelPressed()
return false
}
lastRawX = event.rawX
lastRawY = event.rawY
// If we never applied pressed (quick tap), apply briefly then clear.
// zh-CN: 如果尚未应用 pressed (快速点击), 短暂应用后清除.
if (prePressed && !pressedApplied) {
parent.removeCallbacks(applyPressedRunnable)
if (isInsideViewWithSlop(child, lastRawX, lastRawY, touchSlop, childHitRect)) {
applyPressedNow()
parent.postDelayed(
{ parent.isPressed = false },
pressedStateDuration.toLong(),
)
} else {
cancelPressed()
}
} else {
// Normal end of gesture.
// zh-CN: 正常结束手势.
parent.removeCallbacks(applyPressedRunnable)
// Defer clearing pressed state until after this UP is dispatched to the child view.
// zh-CN: 将清除 pressed 的时机延后到本次 UP 分发给子视图之后.
parent.post {
parent.isPressed = false
}
prePressed = false
pressedApplied = false
}
activePointerId = MotionEvent.INVALID_POINTER_ID
}
MotionEvent.ACTION_CANCEL -> {
cancelPressed()
activePointerId = MotionEvent.INVALID_POINTER_ID
}
}
// Do not consume, so child's click/long-click keep working.
// zh-CN: 不消费事件, 保持子视图 click/long-click 正常工作.
return false
}
/**
* Apply pressed immediately and update hotspot.
* zh-CN: 立即应用 pressed, 并更新 hotspot.
*/
private fun applyPressedNow() {
pressedApplied = true
prePressed = false
parent.isPressed = true
updateHotspot(lastRawX, lastRawY)
}
/**
* Cancel any pending pressed state and clear current pressed state.
* zh-CN: 取消所有待触发的 pressed, 并清除当前 pressed 状态.
*/
private fun cancelPressed() {
parent.removeCallbacks(applyPressedRunnable)
parent.isPressed = false
prePressed = false
pressedApplied = false
}
/**
* Update parent's ripple hotspot based on raw coordinates.
* zh-CN: 基于 raw 坐标更新父视图的 Ripple hotspot.
*/
private fun updateHotspot(rawX: Float, rawY: Float) {
parent.getLocationOnScreen(parentLoc)
val hx = rawX - parentLoc[0]
val hy = rawY - parentLoc[1]
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
parent.drawableHotspotChanged(hx, hy)
parent.background?.setHotspot(hx, hy)
parent.foreground?.setHotspot(hx, hy)
}
}
})
}
}
/**
* Check whether a raw (screen) coordinate is inside the view bounds with slop tolerance.
* zh-CN: 判断 raw (屏幕) 坐标是否在视图边界内, 并允许一定 slop 容差.
*/
private fun isInsideViewWithSlop(
view: View,
rawX: Float,
rawY: Float,
slop: Int,
outRect: Rect,
): Boolean {
val loc = IntArray(2)
view.getLocationOnScreen(loc)
val xInView = rawX - loc[0]
val yInView = rawY - loc[1]
outRect.set(
-slop,
-slop,
view.width + slop,
view.height + slop,
)
return outRect.contains(xInView.toInt(), yInView.toInt())
}
enum class MODE(val key: String) {
DAY(key(R.string.key_night_mode_always_off)),

View File

@@ -5,8 +5,10 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="43dp"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:minHeight="43dp"
android:orientation="horizontal">
<FrameLayout
@@ -32,7 +34,6 @@
android:layout_height="15dp"
android:backgroundTint="@color/drawer_menu_group_text_color"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
@@ -41,7 +42,7 @@
android:layout_width="match_parent"
android:minHeight="43dp"
android:layout_toEndOf="@+id/icon_container"
android:layout_toStartOf="@id/sw"
android:layout_toStartOf="@id/switch_divider"
android:layout_centerVertical="true"
android:layout_marginEnd="5dp"
android:gravity="center_vertical"
@@ -66,9 +67,20 @@
android:visibility="gone"
tools:visibility="gone"
tools:text="@string/text_sample_string" />
</LinearLayout>
<View
android:id="@+id/switch_divider"
android:layout_width="1dp"
android:layout_height="24dp"
android:layout_toStartOf="@id/sw"
android:layout_marginStart="2dp"
android:layout_marginEnd="6dp"
android:layout_centerVertical="true"
android:background="@color/day_night_alpha_20"
android:visibility="gone"
tools:visibility="visible" />
<org.autojs.autojs.ui.widget.PrefSwitch
android:id="@+id/sw"
android:layout_width="wrap_content"
@@ -79,5 +91,4 @@
android:visibility="gone"
tools:checked="true"
tools:visibility="visible" />
</RelativeLayout>

View File

@@ -18,7 +18,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingBottom="4dp"/>
android:paddingBottom="4dp" />
<View
android:layout_width="match_parent"
@@ -29,16 +29,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="2dp"
android:paddingVertical="2dp"
android:baselineAligned="false">
<LinearLayout
android:id="@+id/settings"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?selectableItemBackground"
android:layout_gravity="center|bottom"
android:gravity="center|bottom"
android:orientation="vertical">
<ImageView
@@ -65,10 +65,10 @@
<LinearLayout
android:id="@+id/plugin_center"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?selectableItemBackground"
android:layout_gravity="center|bottom"
android:gravity="center|bottom"
android:orientation="vertical">
<ImageView
@@ -95,10 +95,10 @@
<LinearLayout
android:id="@+id/restart"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?selectableItemBackground"
android:layout_gravity="center|bottom"
android:gravity="center|bottom"
android:orientation="vertical">
<ImageView
@@ -125,10 +125,10 @@
<LinearLayout
android:id="@+id/exit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?selectableItemBackground"
android:layout_gravity="center|bottom"
android:gravity="center|bottom"
android:orientation="vertical">
<ImageView