本文所用到的所有图片资源均存放在“我的相册—>QQ5.0侧滑资源”中

概览

本文将实现Android端 QQ 5.0 App的主界面,在实现的过程中,还会顺带着实现两个侧滑效果的主界面。最终的主界面预览效果如下所示:

android QQ侧边滑动 手机qq侧边栏怎么设置_ViewGroup

分析如下:

  • 主界面包含两部分内容,左边是菜单部分,右边是内容部分,并呈现水平摆放;
  • 界面具有侧滑效果,需要将这两部分内容放在一个具有水平滚动功能的控件中;
  • 若将左侧的菜单只滑出一小部分,则在松开手指时菜单依然平滑隐藏;反之,则将菜单项平滑显示出来,内容区域缩小至右侧。

分析

为了上述实现效果,我们需要一个容器控件将菜单和内容两部分装入,自然想到了自定义ViewGroup来实现,但是继承ViewGroup自定义水平滚动控件需要不断在onTouchEvent的MOVE动作中根据手指当前的滑动位置改变ViewGroup的marginLeft属性、并需要处理滑动冲突问题、同时还要处理手指抬起时菜单回弹或显示的动画效果,这一般使用Scroller辅助类。看得出来,实现起来比较麻烦。

搜遍API,发现Android中提供了一个HorizontalScrollView这个控件,它自带水平滚动属性 —— MOVE事件不再需要自己处理;同时也不再需要处理滑动冲突 —— 该控件中完全可以放一个ListView。我们只需要在onTouchEvent的UP事件中判断滑动的位置就能完美实现上图的效果。

一、实现水平滑动界面

在实现最终效果之前,我们先来实现一个水平滑动的效果。它的效果如下:

android QQ侧边滑动 手机qq侧边栏怎么设置_android QQ侧边滑动_02

首先是菜单的布局XML代码,它包含五个item,同时还有一个背景图,代码如下:

<!-- left_menu.xml -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/img_frame_background">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:orientation="vertical">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/iv_img_1"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_centerVertical="true"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="20dp"
                android:background="@drawable/img_one" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_marginLeft="20dp"
                android:layout_toRightOf="@id/iv_img_1"
                android:text="第一个item"
                android:textColor="#FFFFFF"
                android:textSize="20sp" />


        </RelativeLayout>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/iv_img_2"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_centerVertical="true"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="20dp"
                android:background="@drawable/img_two" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_marginLeft="20dp"
                android:layout_toRightOf="@id/iv_img_2"
                android:text="第二个item"
                android:textColor="#FFFFFF"
                android:textSize="20sp" />


        </RelativeLayout>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/iv_img_3"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_centerVertical="true"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="20dp"
                android:background="@drawable/img_three" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_marginLeft="20dp"
                android:layout_toRightOf="@id/iv_img_3"
                android:text="第三个item"
                android:textColor="#FFFFFF"
                android:textSize="20sp" />


        </RelativeLayout>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/iv_img_4"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_centerVertical="true"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="20dp"
                android:background="@drawable/img_four" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_marginLeft="20dp"
                android:layout_toRightOf="@id/iv_img_4"
                android:text="第四个item"
                android:textColor="#FFFFFF"
                android:textSize="20sp" />


        </RelativeLayout>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/iv_img_5"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_centerVertical="true"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="20dp"
                android:background="@drawable/img_five" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_marginLeft="20dp"
                android:layout_toRightOf="@id/iv_img_5"
                android:text="第五个item"
                android:textColor="#FFFFFF"
                android:textSize="20sp" />


        </RelativeLayout>
    </LinearLayout>

</RelativeLayout>

接着是主界面XML,HorizontalScrollView中包含菜单和内容布局(内容布局就是一张图片),代码如下:

<!-- activity_main_xml -->

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <HorizontalScrollView
        android:id="@+id/smv_custom_sliding_menu"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        vanpersie:menuRightPadding="100dp">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:orientation="horizontal">

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

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@drawable/qq"
                android:orientation="horizontal">

            </LinearLayout>


        </LinearLayout>
    </HorizontalScrollView>
</RelativeLayout>

需要注意的是,HorizontalScrollView只能包含一个直接子控件。

继续,自定义一个SlidingMenuView ,它于继承HorizontalScrollView:

public class SlidingMenuView extends HorizontalScrollView {
    public static final String TAG = "SlidingMenuView";
    //HorizontalScrollView包裹的唯一一个子View
    private LinearLayout mWrapper;
    //菜单栏
    private ViewGroup mMenu;
    //内容区域
    private ViewGroup mContent;
    //屏幕宽度 单位px
    private int mScreenWidth;
    //Menu与屏幕右侧的距离 单位 dp,默认为50dp
    private int mMenuRightPadding = 50;
    //只让onMeasure调用一次
    private boolean once = false;
    //menu的宽度
    private int mMenuWidth;
    //切换菜单隐藏及显示
    private boolean isOpen;


    /**
     * 当未自定义属性时 系统将调用该构造方法
     *
     * @param context
     * @param attrs
     */
    public SlidingMenuView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);


    }

    /**
     * 自定义且使用了自定义属性
     *
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public SlidingMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //获取自定义的属性
        TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SlidingMenuView, defStyleAttr, 0);

        int n = ta.getIndexCount();
        for (int i = 0; i < n; ++i) {
            int attr = ta.getIndex(i);
            switch (attr) {
                case R.styleable.SlidingMenuView_menuRightPadding:
                    //将默认的50dp转化为px
                    mMenuRightPadding = (int) ta.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (float) 50, context.getResources().getDisplayMetrics()));

                    break;
            }
        }

        ta.recycle();

        //获取屏幕宽度
        WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        manager.getDefaultDisplay().getMetrics(outMetrics);
        mScreenWidth = outMetrics.widthPixels;


    }

    public SlidingMenuView(Context context) {
        this(context, null);
    }

    /**
     * 设置子View的宽高、设置自己的宽高
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (!once) {
            //获取HorizontalScrollView中的为一个元素
            mWrapper = (LinearLayout) getChildAt(0);
            //获取菜单
            mMenu = (ViewGroup) mWrapper.getChildAt(0);
            //获取内容
            mContent = (ViewGroup) mWrapper.getChildAt(1);

            //设置菜单的宽度为屏幕的宽度-右边距
            mMenuWidth = mMenu.getLayoutParams().width = mScreenWidth - mMenuRightPadding;

            mContent.getLayoutParams().width = mScreenWidth;
            once = true;

        }


    }

    /**
     * 设置SlidingMenuView的位置
     * 应当将Content内容显示,将Menu隐藏在左侧 需设置偏移量实现
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (changed) {
            //x为正值时滚动条向右移动 内容向左移动
            this.scrollTo(mMenuWidth, 0);
        }


    }




}

分析:

1、首先需要在activity_main.xml布局文件中的HorizontalScrollView标签修改为com.demo.lenovo.qqslidingmenu.SlidingMenuView;

2、还需要为SlidingMenuView自定义一个属性,这个属性表示当显示菜单项时,菜单项与右侧屏幕的距离,也就是内容区域显示的剩余区域的宽度大小:

在res/values/attr.xml中自定义属性,format为dimension,表示该属性可以设置的单位为 dp、sp、px 等:

<resources>
    <attr name="menuRightPadding" format="dimension" />
    <declare-styleable name="SlidingMenuView">
        <attr name="menuRightPadding" />

    </declare-styleable>

</resources>

在com.demo.lenovo.qqslidingmenu.SlidingMenuView标签下声明自定义属性的命名空间,格式有两种:

第一种是:
xmlns:任意名字=”http://schemas.android.com/apk/res-auto”

第二种是:
xmlns:任意名字=”http://schemas.android.com/apk/res/包名”
即:
xmlns:vanpersie=”http://schemas.android.com/apk/res/com.demo.lenovo.qqslidingmenu”

并设置属性值为100dp:

vanpersie:menuRightPadding="100dp"

最后在代码在三个参数的构造方法中获取该属性值,我们将该属性值的默认值设为50dp,并将单位转化为px:

3、在构造方法中通过DisplayMetrics 类获取屏幕宽度:

//获取屏幕宽度
        WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        manager.getDefaultDisplay().getMetrics(outMetrics);
        mScreenWidth = outMetrics.widthPixels;

4、重写onMeasure()方法,在该方法中,需要测量ViewGroup中子控件的宽和高以及自己的宽和高;由于只需要测量一次,故设置一个标志位once,控制onMeasure()方法只回调一次;

5、重写onLayout()方法,该方法用于设置SlidingMenuView的位置,初始时,我们将菜单隐藏;故需要调用scrollTo(mMenuWidth, 0);,将菜单滑出屏幕;

6、最后,重写onTouchEvent()方法,在UP事件中,监听水平滚动的位置,当getScrollX() 大于菜单宽度的一半时,隐藏菜单;反之,显示菜单:

case MotionEvent.ACTION_UP:
                int scrollX = getScrollX();
                //这时应将菜单隐藏
                if (scrollX >= (mMenuWidth / 2)) {
                    this.smoothScrollTo(mMenuWidth, 0);
                } else {
                    smoothScrollTo(0, 0);

                }

到此,水平滑动效果实现完成。

二、实现抽屉效果界面

抽屉式效果入下:

android QQ侧边滑动 手机qq侧边栏怎么设置_Horizontal_03

为了实现抽屉效果,需要重写onScrollChanged()方法,该方法的第一参数就是scrollX的值,该值表示菜单项的左侧与屏幕左侧的距离,而属性动画的setTranslationX(float translationX) 方法中参数translationX正好表示控件的水平偏移量,故只需加入这几行代码,抽屉效果即可实现:

@Override
    protected void onScrollChanged(final int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);


        mMenu.setTranslationX((float) (l));
}

三、带有缩放、透明度渐变的效果最终界面

最终效果:

android QQ侧边滑动 手机qq侧边栏怎么设置_ViewGroup

从最终效果界面看出:

1、菜单区域在横向和纵向有一个大概0.7 ~ 1.0 的缩放效果;

2、菜单区域透明度有一个大概0.6 ~ 1.0 的变化;

3、内容区域有在横向和纵向有一个大概 1.0 ~ 0.7的缩放效果;

4、内容区域的缩放中心默认为View的中心,而此时的缩放中心应该在View的左边缘的中心。

5、当菜单显示时,点击内容区域的参与部分,内容区域应显示,菜单隐藏。

我们需要一个梯度变化值scale,该值根据onScrollChanged的第一个参数l(即getScrollX())而来:

float scale = l * 1.0f / mMenuWidth; // 1.0 ~ 0.0

接着,根据不同的该值计算不同的梯度:

float rightScale = 0.7f + 0.3f * scale; // 1.0 ~ 0.7
        float leftScale = 1.0f - scale * 0.3f;  //0.7 ~ 1.0
        float leftAlpha = 0.6f + 0.4f * (1 - scale); // 0.6 ~ 1.0

并利用属性动画进行设置:

mMenu.setScaleX(leftScale);
        mMenu.setScaleY(leftScale);
        mMenu.setAlpha(leftAlpha);

        mContent.setPivotX(0);
        mContent.setPivotY(mContent.getHeight() / 2);
        mContent.setScaleX(rightScale);
        mContent.setScaleY(rightScale);

最后为内容区域设置监听:

mContent.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                if (l == 0) {
                    SlidingMenuView.this.smoothScrollTo(mMenuWidth, 0);
                }

            }
        });

注意:为了保证滑动时背景不发生改变,我们应将最初设置在left_menu.xml的background背景图设置到activity_main.xml中。

至此,所有效果已经完全实现

总结

要点:
一、自定义ViewGroup的方法:

1、选择合适的构造方法,获得一些需要用到的值;

2、onMeasure 计算子View的宽和高,以及设置自己的宽和高;

3、onLayout 决定子View的布局的位置;

4、需要监听ViewGroup的滑动事件时,应重写onTouchEvent()方法;否则无需重写该方法。

二、构造方法

1、有三个构造方法:一般情况为 使用一个参数的调用两个的, 两个的调用三个的;

2、一个参数的构造方法用于在代码中动态new时初始化(如Button btn = new Button(context));

3、两个参数的构造方法用于没有设置自定义属性时的初始化;

4、三个参数的构造方法用于设置自定义属性、并在代码中使用该属性时的初始化。

三、自定义属性
1、在attr.xml中自定义属性并指明其单位;
2、布局文件中正确书写命名空间的值 xmlns;
3 、在三个参数的构造方法中获取

四、属性动画 缩放、透明度 等。