diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index e9016aa6..03da199e 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -151,7 +151,7 @@ "崩溃报告页面支持双指缩放调整字体大小并添加常用功能按钮", "应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_", "Rhino 引擎在泛型签名解析失败时回退为原始反射类型以增强低版本安卓系统的反射方法可用性", - "主页抽屉开关类条目支持点击标题文字区域显示详情对话框并按需支持快捷跳转系统设置", + "主页抽屉开关类条目添加分隔线, 支持点击标题文字区域显示详情对话框并按需支持快捷跳转系统设置", "启动或重启 AutoJs6 时支持点击主页抽屉 \"客户端模式\" 标题文字区域中止正在尝试建立的连接", "客户端模式连接时支持使用 IPv6 地址及域名地址建立连接", "客户端模式连接时支持特殊用途 IPv4 地址 (回环/广播/多播/保留/...) 检测提示", diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt index d965287a..b7a432e8 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt @@ -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() { diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemViewHolder.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemViewHolder.kt index 402678fb..f11a16a7 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemViewHolder.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuItemViewHolder.kt @@ -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(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 { menuItem.launchManagerIfPossible() @@ -59,7 +69,7 @@ class DrawerMenuItemViewHolder(itemView: View) : BindableViewHolder { menuItem.onTitleContainerClick() @@ -68,7 +78,7 @@ class DrawerMenuItemViewHolder(itemView: View) : BindableViewHolder { menuItem.launchManagerIfPossible() @@ -98,9 +108,11 @@ class DrawerMenuItemViewHolder(itemView: View) : BindableViewHolder + 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)), diff --git a/app/src/main/res/layout/drawer_menu_item.xml b/app/src/main/res/layout/drawer_menu_item.xml index 5b9b1a7d..01761348 100644 --- a/app/src/main/res/layout/drawer_menu_item.xml +++ b/app/src/main/res/layout/drawer_menu_item.xml @@ -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"> - - + + - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_drawer.xml b/app/src/main/res/layout/fragment_drawer.xml index a45e319e..e9a5e7f1 100644 --- a/app/src/main/res/layout/fragment_drawer.xml +++ b/app/src/main/res/layout/fragment_drawer.xml @@ -18,7 +18,7 @@ android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" - android:paddingBottom="4dp"/> + android:paddingBottom="4dp" />