本文所用到的所有图片资源均存放在“我的相册—>QQ5.0侧滑资源”中
概览
本文将实现Android端 QQ 5.0 App的主界面,在实现的过程中,还会顺带着实现两个侧滑效果的主界面。最终的主界面预览效果如下所示:
分析如下:
- 主界面包含两部分内容,左边是菜单部分,右边是内容部分,并呈现水平摆放;
- 界面具有侧滑效果,需要将这两部分内容放在一个具有水平滚动功能的控件中;
- 若将左侧的菜单只滑出一小部分,则在松开手指时菜单依然平滑隐藏;反之,则将菜单项平滑显示出来,内容区域缩小至右侧。
分析
为了上述实现效果,我们需要一个容器控件将菜单和内容两部分装入,自然想到了自定义ViewGroup来实现,但是继承ViewGroup自定义水平滚动控件需要不断在onTouchEvent的MOVE动作中根据手指当前的滑动位置改变ViewGroup的marginLeft属性、并需要处理滑动冲突问题、同时还要处理手指抬起时菜单回弹或显示的动画效果,这一般使用Scroller辅助类。看得出来,实现起来比较麻烦。
搜遍API,发现Android中提供了一个HorizontalScrollView这个控件,它自带水平滚动属性 —— MOVE事件不再需要自己处理;同时也不再需要处理滑动冲突 —— 该控件中完全可以放一个ListView。我们只需要在onTouchEvent的UP事件中判断滑动的位置就能完美实现上图的效果。
一、实现水平滑动界面
在实现最终效果之前,我们先来实现一个水平滑动的效果。它的效果如下:
首先是菜单的布局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);
}
到此,水平滑动效果实现完成。
二、实现抽屉效果界面
抽屉式效果入下:
为了实现抽屉效果,需要重写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));
}
三、带有缩放、透明度渐变的效果最终界面
最终效果:
从最终效果界面看出:
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 、在三个参数的构造方法中获取
四、属性动画 缩放、透明度 等。