自定义控件-侧边菜单SlidingMenu(滑动菜单)

1.布局

 activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.slidemenu.MainActivity" >

    <com.example.slidemenu.SlideMenu
        android:id="@+id/sm"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

       <!-- 引入菜单布局, 索引为: 0 -->

        <include layout="@layout/slide_menu" />

       <!-- 引入菜单布局, 索引为: 0 -->

        <include layout="@layout/slide_content" />
    </com.example.slidemenu.SlideMenu>

</RelativeLayout>

这个布局文件用到了自定义的ViewGroup对象com.example.slidemenu.SlideMenu

继承了ViewGroup类的控件时可以在其内部嵌套子控件。

slide_content.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <LinearLayout 
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:background="@drawable/top_bar_bg"
        android:gravity="center_vertical"
        >
        <ImageView
            android:id="@+id/iv" 
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/main_back"
            />
        <TextView 
            android:layout_width="1dp"
            android:layout_height="match_parent"
            android:background="@drawable/top_bar_divider"
            android:layout_margin="5dp"
            />
        <TextView 
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="新闻"
            android:textSize="30sp"
            android:textColor="#ffffff"
            />
    </LinearLayout>
    <TextView 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:textSize="30sp"
        android:text="!!!!!!!!!!!"
        android:gravity="center"
        />
</LinearLayout>

slide_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="240dp"
    android:layout_height="match_parent" >

    <LinearLayout
        android:layout_width="240dp"
        android:layout_height="match_parent"
        android:background="@drawable/menu_bg"
        android:orientation="vertical" >
        
        <TextView 
            android:text="新闻"
            android:drawableLeft="@drawable/tab_news"
            style="@style/SlideMenuStyle"
            />
         <TextView 
            android:text="订阅"
            android:drawableLeft="@drawable/tab_read"
            style="@style/SlideMenuStyle"
            />
        <TextView 
            android:text="本地"
            android:drawableLeft="@drawable/tab_local"
            style="@style/SlideMenuStyle"
            />
        <TextView 
            android:text="跟帖"
            android:drawableLeft="@drawable/tab_ties"
            style="@style/SlideMenuStyle"
            />
        <TextView 
            android:text="图片"
            android:drawableLeft="@drawable/tab_pics"
            style="@style/SlideMenuStyle"
            />
        <TextView 
            android:text="话题"
            android:drawableLeft="@drawable/tab_ugc"
            style="@style/SlideMenuStyle"
            />
        <TextView 
            android:text="投票"
            android:drawableLeft="@drawable/tab_vote"
            style="@style/SlideMenuStyle"
            />
        <TextView 
            android:text="聚合阅读"
            android:drawableLeft="@drawable/tab_focus"
            style="@style/SlideMenuStyle"
            />
    </LinearLayout>
    

</ScrollView>

/res/values/styles.xml

<resources>
    <style name="AppBaseTheme" parent="Theme.AppCompat.Light">
    </style>
    <!-- Application theme. -->
    <style name="AppTheme" parent="AppBaseTheme">
    </style>

    <!-- slideMenu style -->
    <style name="SlideMenuStyle">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:gravity">center_vertical</item>
        <item name="android:textSize">18sp</item>
        <item name="android:textColor">#ffffff</item>
        <item name="android:background">@drawable/selector_slidemenuitem_bg</item>
        <item name="android:padding">10dp</item>
        <item name="android:drawablePadding">20dp</item>
        <item name="android:onClick">click</item>
        <item name="android:clickable">true</item>
    </style>

</resources>

res/drawable-hdpi/selector_slidemenuitem_bg.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<!--     @android:color/transparent为透明色 -->
    <item android:drawable="@android:color/transparent" android:state_pressed="true"></item>
    <item android:drawable="@color/slidemenu_press_bg" android:state_pressed="false"></item>
    
</selector>

res/values/colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="slidemenu_press_bg">#3333cccc</color>
</resources>

 

业务逻辑实现

 

自定义ViewGroup控件

 

 Android事件处理机制

--------------------------------------------------

自定义类SlideMenu继承ViewGroup。该类作为自定义控件类。

SlideMenu.java

package com.example.slidemenu;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

public class SlideMenu extends ViewGroup {

    private int downX;// 按下时x轴的偏移量
    private int menuMeasuredWidth;
    int currentState = STATE_MAIN_VIEW;// 当前屏幕显示的界面, 默认为: 主界面
    static final int STATE_MENU_VIEW = 0;// 菜单界面
    static final int STATE_MAIN_VIEW = 1;// 主界面
    // 利用这个对象做出滑动的动画效果
    Scroller scroller;

    public SlideMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
        // TODO Auto-generated constructor stub
        scroller = new Scroller(context);
    }

    // 测量子节点的大小
    /**
     * widthMeasureSpec 填充屏幕 heightMeasureSpec 填充屏幕
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // TODO Auto-generated method stub
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 调用子节点的measure方法,该方法实际是调用了子节点的onMeasure方法,测量子节点的大小
        // 测量菜单的宽和高. 宽: 240dip, 高: 填充屏幕
        View menuView = getChildAt(0);
        menuView.measure(menuView.getLayoutParams().width, heightMeasureSpec);
        // 测量主界面的宽和高. 宽: 填充屏幕, 高: 填充屏幕
        View mainView = getChildAt(1);
        mainView.measure(widthMeasureSpec, heightMeasureSpec);
    }

    // 分配空间
    // 因为slideMenu占满了父元素,宽高与父元素相同
    // l:0
    // t:0
    // r:slideMenu的宽,也就是父元素的宽
    // b:slideMenu的高,也就是父元素的高
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // TODO Auto-generated method stub
        // 主界面的位置放置在屏幕左上角
        View mainView = getChildAt(1);
        mainView.layout(l, t, r, b);
        // 把菜单的位置放置在屏幕的左侧
        View menuView = getChildAt(0);
        menuMeasuredWidth = menuView.getMeasuredWidth();
        menuView.layout(-menuView.getMeasuredWidth(), t, 0, b);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 只有在横着滑动时才可以拦截.
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downX = (int) ev.getX();
            break;
        case MotionEvent.ACTION_MOVE:
            int moveX = (int) ev.getX();
            if (Math.abs(moveX - downX) > 10) {
                return true;
            }
            break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // TODO Auto-generated method stub
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downX = (int) event.getX();

            break;

        case MotionEvent.ACTION_MOVE:
            int moveX = (int) event.getX();
            int offsetX = downX - moveX;

            // 判断一下用户手指滑动是否越界
            // 判断给定当前的增量移动后, 是否能够超出边界.
            int scrollX = getScrollX();
            if (scrollX + offsetX < -menuMeasuredWidth) {
                // 当前超出了左边界, 应该设置为在菜单的左边界位置上.
                scrollTo(-menuMeasuredWidth, 0);
            } else if (scrollX + offsetX > 0) {
                // 当前超出了右边界, 应该设置为0
                scrollTo(0, 0);
            } else {
                scrollBy(offsetX, 0);
            }
            downX = moveX;

            break;
        case MotionEvent.ACTION_UP:
            // 获取菜单宽度的一半
            int center = -menuMeasuredWidth / 2;
            scrollX = getScrollX();// 当前屏幕左上角的值
            if (scrollX < center) {
                currentState = STATE_MENU_VIEW;
                refreshViewState();
            } else {
                currentState = STATE_MAIN_VIEW;
                refreshViewState();
            }

            break;

        }
        return true;
    }

    // 松手时根据滑动进度,让mScrollX复位
    /**
     * 根据currentScreen变量来切换屏幕显示
     */
    private void refreshViewState() {
        // TODO Auto-generated method stub
        int startX = getScrollX();// 开始的位置
        int dx = 0;// 增量值 = 目的地位置 - 开始的位置;

        if (currentState == STATE_MAIN_VIEW) {
            dx = 0 - startX;
        } else if (currentState == STATE_MENU_VIEW) {
            dx = -menuMeasuredWidth - startX;
        }

        int duration = Math.abs(dx) * 10;
        if (duration > 1600) {
            duration = 1600;
        }
        // arg0:滑动的开始坐标
        // arg1:滑动的距离
        scroller.startScroll(startX, 0, dx, 0, duration);

        // 只要调用invalidate,那么computeScroll方法就会调用
        // 刷新当前控件, 会引起onDraw方法的调用.
        invalidate();// -> drawChild -> view.draw -> view.computeScroll
    }

    @Override
    public void computeScroll() {
        // scroller不会去改变mScrollX的值,它只是帮你计算出你应该把mScrollX的值变为多少
        // 判断动画是否结束,如果时间到了,那么结束了,返回false,如果时间未到,返回true
        if (scroller.computeScrollOffset()) {

            // 当前mScrollX应该变为多少,这个返回值是startX+应该位移的距离
            /*
             * scroller .startScroll(0 , 0 , 1000, 0,100);
             * 
             * 当时间过去了1毫秒时,屏幕应该移动10像素 当时间过去了10毫秒时,屏幕应该移动100像素 例如:
             * 3点10分50000毫秒,调用那个了scroller.startScroll(0 , 0 , 1000, 0,100);
             * 3点10分50010毫秒,调用那个了scroller.getcurrx(),返回100
             * 3点10分50050毫秒,调用那个了scroller.getcurrx(),返回500
             */
            int currX = scroller.getCurrX();
            scrollTo(currX, 0);

            invalidate();// 在触发当前方法, 相当于递归.
        }
    }

    /**
     * 是否显示菜单
     * 
     * @return true 显示菜单, false 不显示
     */
    public boolean isShowMenu() {
        return currentState == STATE_MENU_VIEW;
    }

    /**
     * 隐藏菜单
     */
    public void hideMenuView() {
        currentState = STATE_MAIN_VIEW;
        refreshViewState();
    }

    /**
     * 显示菜单
     */
    public void showMenuView() {
        currentState = STATE_MENU_VIEW;
        refreshViewState();
    }
}

MainActivity.xml

package com.example.slidemenu;

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);

        final SlideMenu sm = (SlideMenu) findViewById(R.id.sm);
        ImageView iv = (ImageView) findViewById(R.id.iv);
        iv.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                // 判断当前状态
                if (sm.isShowMenu()) {
                    // 是菜单界面, 切换到主界面
                    sm.hideMenuView();
                } else {
                    // 是主界面, 应该把菜单显示出来
                    sm.showMenuView();
                }
            }
        });
    }

    public void click(View v) {
        Toast.makeText(this, ((TextView) v).getText(), 0).show();
    }

}

 

posted @ 2016-05-31 15:20  沉默的羊癫疯  阅读(591)  评论(0编辑  收藏  举报