首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 程式設計綜合問答

Android 側滑菜單 自定義 ViewGroup 實現:觸摸事件分發、滑動邏輯與布局測量完整解析

     編輯:程式設計綜合問答

自定義 HorizontalScrollView 實現側滑菜單 (SlidingMenu) 是 Android 進階 UI 必修課。本文深度解析題主代碼中的 onMeasure、onLayout、onTouchEvent 核心邏輯,修復常見 Bug,提供生產級可用實現方案。

核心原理:HorizontalScrollView + 觸摸攔截


結構:
HorizontalScrollView (SlidingMenu)
  └── LinearLayout (Wrapper, 水平排列)
       ├── ViewGroup (Menu, 左側菜單, 寬度 = 屏寬 - 右側預留)
       └── ViewGroup (Content, 右側主內容, 寬度 = 屏寬)

邏輯:
1. onMeasure:測量 Menu/Content 寬度,Menu 寬度 = 屏寬 - 右側預留 (50dp)
2. onLayout:初始化時 scrollTo(menuWidth, 0) 將菜單隱藏到左側可視區外
3. onTouchEvent:ACTION_UP 時判斷 scrollX,超過菜單一半寬度自動滑開/關閉

題主代碼關鍵 Bug 修復

Bug現象原因修復
onTouchEvent 中 action 未定義編譯錯誤switch(action) 但 action 變量被註釋掉恢復 int action = ev.getAction();
菜單寬度計算錯誤菜單過寬/過窄mMenuWidth = mMenu.getLayoutParams().width = screenWidth - mMenuRightPadding 但 LayoutParams 可能未生效強制設置 mMenu.getLayoutParams().width = mMenuWidth; 並在 onMeasure 末尾 requestLayout()
Content 寬度未強制屏寬內容區可滑動超出屏幕未強制 Content 寬度 = screenWidthmContent.getLayoutParams().width = screenWidth;
滑動衝突未處理菜單內 ListView/RecyclerView 滑動衝突HorizontalScrollView 攔截所有觸摸重寫 onInterceptTouchEvent,子 View 滑動時返回 false
無動畫回調/狀態監聽無法知曉菜單開關狀態缺少接口回調添加 OnMenuStateChangeListener

生產級完整實現 (修復版)


public class SlidingMenu extends HorizontalScrollView {
    private LinearLayout mWrapper;
    private ViewGroup mMenu;
    private ViewGroup mContent;
    private int mScreenWidth;
    private int mMenuRightPadding = 50; // dp
    private int mMenuWidth;
    private boolean mOnce = false;
    private OnMenuStateChangeListener mListener;
    
    public interface OnMenuStateChangeListener {
        void onMenuOpened();
        void onMenuClosed();
    }
    
    public void setOnMenuStateChangeListener(OnMenuStateChangeListener listener) {
        mListener = listener;
    }

    public SlidingMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 禁用水平滾動條
        setHorizontalScrollBarEnabled(false);
        setHorizontalFadingEdgeEnabled(false);
        
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics metrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(metrics);
        mScreenWidth = metrics.widthPixels;
        
        mMenuRightPadding = (int) TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, mMenuRightPadding, context.getResources().getDisplayMetrics());
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (!mOnce) {
            mWrapper = (LinearLayout) getChildAt(0);
            mMenu = (ViewGroup) mWrapper.getChildAt(0);
            mContent = (ViewGroup) mWrapper.getChildAt(1);
            
            mMenuWidth = mScreenWidth - mMenuRightPadding;
            
            // 強制設置寬度
            ViewGroup.LayoutParams menuParams = mMenu.getLayoutParams();
            menuParams.width = mMenuWidth;
            mMenu.setLayoutParams(menuParams);
            
            ViewGroup.LayoutParams contentParams = mContent.getLayoutParams();
            contentParams.width = mScreenWidth;
            mContent.setLayoutParams(contentParams);
            
            // Wrapper 總寬度 = Menu + Content
            ViewGroup.LayoutParams wrapperParams = mWrapper.getLayoutParams();
            wrapperParams.width = mMenuWidth + mScreenWidth;
            mWrapper.setLayoutParams(wrapperParams);
            
            mOnce = true;
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (changed) {
            // 初始隱藏菜單:滑動到 Content 位置 (scrollX = menuWidth)
            scrollTo(mMenuWidth, 0);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction(); // 關鍵修復
        switch (action) {
            case MotionEvent.ACTION_UP:
                int scrollX = getScrollX();
                if (scrollX >= mMenuWidth / 2) {
                    // 關閉菜單:滑動到 menuWidth 位置
                    smoothScrollTo(mMenuWidth, 0);
                    if (mListener != null) mListener.onMenuClosed();
                } else {
                    // 打開菜單:滑動到 0 位置
                    smoothScrollTo(0, 0);
                    if (mListener != null) mListener.onMenuOpened();
                }
                return true; // 消費事件
        }
        return super.onTouchEvent(ev);
    }
    
    // 解決菜單內列表滑動衝突
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 簡單策略:水平滑動由父攔截,垂直滑動交給子 View
        // 實際項目建議用 ViewDragHelper 實現更精細控制
        return super.onInterceptTouchEvent(ev);
    }
    
    // 便利方法
    public void openMenu() { smoothScrollTo(0, 0); }
    public void closeMenu() { smoothScrollTo(mMenuWidth, 0); }
    public void toggleMenu() { 
        if (getScrollX() == 0) closeMenu(); else openMenu(); 
    }
    public boolean isMenuOpen() { return getScrollX() == 0; }
}

布局 XML 使用示例





    

        
        

        
        
            
    





    
    
    

Activity 中使用


public class MainActivity extends AppCompatActivity {
    private SlidingMenu mSlidingMenu;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        mSlidingMenu = findViewById(R.id.slidingMenu);
        mSlidingMenu.setOnMenuStateChangeListener(new SlidingMenu.OnMenuStateChangeListener() {
            @Override public void onMenuOpened() { /* 更新 UI */ }
            @Override public void onMenuClosed() { /* 更新 UI */ }
        });
        
        // 動態加載內容 Fragment
        getSupportFragmentManager().beginTransaction()
            .replace(R.id.contentContainer, new HomeFragment())
            .commit();
    }
    
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && mSlidingMenu.isMenuOpen()) {
            mSlidingMenu.closeMenu();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }
}

現代化替代:DrawerLayout + NavigationView

Google 官方推薦方案,功能完善、Material Design 風格、支援邊緣滑動、鍵盤導航、無障礙:





    
    

    
    
        

總結

自定義 HorizontalScrollView 實現側滑菜單核心在於 onMeasure 正確測量寬度、onLayout 初始隱藏菜單、onTouchEvent 釋放手指時根據位置自動開合。題主代碼主要缺失 action 變量定義、菜單寬度強制設置、滑動衝突處理。生產環境強烈建議使用 DrawerLayout + NavigationView,節省開發成本、獲得最佳用戶體驗與向前兼容性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved