概述
本篇只是个示例,理解本篇博客后,可实现仿QQ5.0侧滑,左右两侧滑动菜单。再加上各种缩放,平移特效。DuangDuang的。本篇效果如下:
实现步骤
- 因为需要水平滑动,所以继承HorizontalScrollView
- 本Domo分为两个部分mMainLayout和mRightLayout。在onMeasure初始化这两部分的宽度
- 在onTouchEvent中判断是否完全展示,拦截当前触摸事件
- 前三步已经实现最简单的滑动布局,最关键的是第四步。mMainLayout跟随手势不断滑动,实现抽屉菜单。
- 自己再加各种特效。
开启侧滑之旅
package com.example.chouti;
import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
public class SlidingRightView extends HorizontalScrollView {
/**
* 主布局界面
*/
private ViewGroup mMainLayout;
/**
* 侧滑界面
*/
private ViewGroup mRightLayout;
/**
* 侧滑界面宽度
*/
private int mRightLayoutWidth;
/**
* 侧滑界面距离屏幕左边的距离
*/
private int mRightLayoutMarginLeft = 200; //px
/**
* 是否展示侧滑
*/
private boolean isOpen;
private int mScreenWidth;
private int mScreenHeight;
private Context mContext;
public SlidingRightView(Context context) {
this(context, null);
}
public SlidingRightView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlidingRightView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView(context);
}
private void initView(Context context) {
mContext = context;
getScreenWidthAndHeight();
}
private void getScreenWidthAndHeight() {
WindowManager wm = (WindowManager) mContext.getSystemService(mContext.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
mScreenWidth = outMetrics.widthPixels;
mScreenHeight = outMetrics.heightPixels;
Log.i("TAG", "mScreenWidth=" + mScreenWidth + "mScreenHeight=" + mScreenHeight);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
LinearLayout v = (LinearLayout) this.getChildAt(0);
mMainLayout = (ViewGroup) v.getChildAt(0);
mRightLayout = (ViewGroup) v.getChildAt(1);
mMainLayout.getLayoutParams().width = mScreenWidth;
mRightLayout.getLayoutParams().width = mRightLayoutWidth = mScreenWidth - mRightLayoutMarginLeft;
Log.i("TAG", " mRightLayout.getLayoutParams().width=" + mRightLayout.getLayoutParams().width);
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) {
this.scrollTo(0, 0);
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_UP:
// scrollX为水平滚动条滚动的宽度
int scrollX = getScrollX();
if (scrollX >= mRightLayoutWidth / 2) {
this.smoothScrollTo(mRightLayoutWidth, 0);
isOpen = true;
} else {
this.smoothScrollTo(0, 0);
isOpen = false;
}
// 拦截事件
return true;
}
return super.onTouchEvent(ev);
}
/**
* 抽屉开关
*/
public void toggleRightLayout() {
if (isOpen) {
closeRightLayout();
isOpen = false;
} else {
openRightLayout();
isOpen = true;
}
}
/**
* 打开抽屉
*/
public void openRightLayout() {
if (isOpen) {
return;
}
this.smoothScrollTo(mRightLayoutWidth, 0);
isOpen = true;
}
/**
* 关闭抽屉
*/
public void closeRightLayout() {
if (!isOpen) {
return;
}
this.smoothScrollTo(0, 0);
isOpen = false;
}
}
上文是前三步工程,完成上面三步之后我们已经有一个基础的滑动布局了。下面对上段代码进行解析。可以看到我们首先获取了屏幕的宽高。在onMeasure中我们为SlingRightView唯一childView的第一个childView和第二个childView宽赋值。在onLayout中默认不显示右侧布局。在onTouchEvent中判断滑动的宽度,如果大于右侧布局宽度的一半,则完全展示右侧布局。否则,不展示右侧布局。拦截当前事件。自定义View流程和拦截点击事件不太了解的同学可以看下我的另外两篇博客自定义View总结 Android 事件传递机制
关键的第四步
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
float scale = l * 1.0f / mRightLayoutWidth;// 0.0~1.0
// 将mMainLayout平移l(第一个参数) 实现抽屉式侧滑菜单
// 这是个相对运动的过程 一定要注意理解
mMainLayout.setTranslationX(mRightLayoutWidth * scale);
}
两行代码实现抽屉式菜单。 第一个参数l为HorizontalScrollView偏移的长度(其实和getScrollX等价) 初始化时没有滑动,此时l=0。这里使用了属性动画。因为是右边布局滑动,主布局需要跟随手势滑动,滑动距离等于右侧布局的宽度。主布局看起来就像是一直“固定”在页面一样,其实是动态的滑动。mMainLayout.setTranslationX(mRightLayoutWidth * scale);
抽屉的精髓就是这行代码,一定要理解这个相对运动。
布局文件示例:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.chouti.SlidingRightView
android:id="@+id/sliding"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/mainLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff0000"
android:orientation="vertical">
<Button
android:id="@+id/open"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="open"
android:textAllCaps="false" />
<Button
android:id="@+id/close"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="close"
android:textAllCaps="false" />
<Button
android:id="@+id/toggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="toggle"
android:textAllCaps="false" />
</LinearLayout>
<LinearLayout
android:id="@+id/rightLayout"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="#00ff00"
android:orientation="vertical">
<Button
android:id="@+id/test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="test" />
</LinearLayout>
</LinearLayout>
</com.example.chouti.SlidingRightView>
</LinearLayout>
MainActivity调用
package com.example.chouti;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.Toast;
public class MainActivity extends Activity {
private SlidingRightView slidingRightView;
@Override
protected void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
slidingRightView = (SlidingRightView) findViewById(R.id.sliding);
findViewById(R.id.open).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
slidingRightView.openRightLayout();
}
});
findViewById(R.id.close).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
slidingRightView.closeRightLayout();
}
});
findViewById(R.id.toggle).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
slidingRightView.toggleRightLayout();
}
});
findViewById(R.id.test).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "click test", Toast.LENGTH_SHORT).show();
}
});
}
}
DuangDuang特效加起来
想要实现更酷炫的特效当然要用到动画。比如将抽屉进行缩放啦,透明度的变化了。具体看项目需求,下面是个简单的示例。
// 为mRightLayout设置缩放和透明度的变化
mRightLayout.setScaleX(mRightLayoutScale);
mRightLayout.setScaleY(mRightLayoutScale);
mRightLayout.setAlpha(mRightLayoutAlpha);
右滑抽屉式菜单到此已经完毕,感谢耐心读完。再次重申:这只是个Demo。现在假设需要左滑抽屉、左右滑抽屉、甚至更变态的各种滑,如果你有思路那么本篇博客的目的达到了。如果没有,建议重新阅读本篇博客。
第二次更新
左滑菜单
思路和右滑一样,直接贴代码了
/**
* Created by Administrator on 2016/4/2.
*/
public class SlidingRightView extends HorizontalScrollView {
/**
* 左侧布局界面
*/
private ViewGroup mLeftLayout;
/**
* 主界面
*/
private ViewGroup mMainLayout;
/**
* 侧滑界面宽度
*/
private int mLeftLayoutWidth;
/**
* 侧滑界面距离屏幕右边的距离
*/
private int mLeftLayoutMarginRight = 200; //px
/**
* 是否展示侧滑
*/
private boolean isOpen;
private int mScreenWidth;
private int mScreenHeight;
private Context mContext;
public SlidingRightView(Context context) {
this(context, null);
}
public SlidingRightView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlidingRightView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView(context);
}
private void initView(Context context) {
mContext = context;
getScreenWidthAndHeight();
}
private void getScreenWidthAndHeight() {
WindowManager wm = (WindowManager) mContext.getSystemService(mContext.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
mScreenWidth = outMetrics.widthPixels;
mScreenHeight = outMetrics.heightPixels;
Log.i("TAG", "mScreenWidth=" + mScreenWidth + "mScreenHeight=" + mScreenHeight);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
LinearLayout v = (LinearLayout) this.getChildAt(0);
mLeftLayout = (ViewGroup) v.getChildAt(0);
mMainLayout = (ViewGroup) v.getChildAt(1);
mLeftLayout.getLayoutParams().width = mLeftLayoutWidth = mScreenWidth - mLeftLayoutMarginRight;
mMainLayout.getLayoutParams().width = mScreenWidth;
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) {
this.scrollTo(mLeftLayoutWidth, 0);
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_UP:
// scrollX为水平滚动条滚动的宽度
int scrollX = getScrollX();
if (scrollX >= mLeftLayoutWidth / 2) {
this.smoothScrollTo(mLeftLayoutWidth, 0);
isOpen = false;
} else {
this.smoothScrollTo(0, 0);
isOpen = true;
}
// 拦截事件
return true;
}
return super.onTouchEvent(ev);
}
/**
* 抽屉开关
*/
public void toggleRightLayout() {
if (isOpen) {
closeRightLayout();
isOpen = false;
} else {
openRightLayout();
isOpen = true;
}
}
/**
* 打开抽屉
*/
public void openRightLayout() {
if (isOpen) {
return;
}
this.smoothScrollTo(0, 0);
isOpen = true;
}
/**
* 关闭抽屉
*/
public void closeRightLayout() {
if (!isOpen) {
return;
}
this.smoothScrollTo(mLeftLayoutWidth, 0);
isOpen = false;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
float scale = l * 1.0f / mLeftLayoutWidth;// 1.0~0.0
mLeftLayout.setTranslationX(l);
}
}
第三次更新
不一样的左滑
应评论要求,在第二次更新中,写了一个左滑抽屉。但是有个问题:无论滑动的是mLeftLayout还是mMainLayout,总是mMainLayout覆盖在mLeftLayout上面(右边覆盖左边)。假设我们要实现mLeftLayout覆盖在mMainLayout上面要怎么做呢?
写自定义View的时候千万不能狭隘,既然这样行不通,何不换一种写法?
package com.dyk.left;
public class LeftSlidingView extends RelativeLayout {
private static final String TAG = "LeftSlidingView";
/** 左侧布局界面 */
private ViewGroup mLeftLayout;
/** 主界面 */
private ViewGroup mMainLayout;
/** 侧滑界面宽度 */
private int mLeftLayoutWidth;
/** 侧滑界面距离屏幕右边的距离 */
private int mLeftLayoutMarginRight = 200; // px
/** 是否展示侧滑 */
private boolean isOpen;
private int mScreenWidth;
private int mScreenHeight;
private Context mContext;
private Point mStartPoint;
private Point mStopPoint;
/** 初始位置记录 */
private int[] location;
private int dx;
public LeftSlidingView(Context context) {
this(context, null);
}
public LeftSlidingView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LeftSlidingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context) {
mContext = context;
mStartPoint = new Point();
mStopPoint = new Point();
getScreenWidthAndHeight();
}
private void getScreenWidthAndHeight() {
WindowManager wm = (WindowManager) mContext.getSystemService(mContext.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
mScreenWidth = outMetrics.widthPixels;
mScreenHeight = outMetrics.heightPixels;
Log.i("TAG", "mScreenWidth=" + mScreenWidth + "mScreenHeight=" + mScreenHeight);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mMainLayout = (ViewGroup) this.getChildAt(0);
mLeftLayout = (ViewGroup) this.getChildAt(1);
mLeftLayout.getLayoutParams().width = mLeftLayoutWidth = mScreenWidth - mLeftLayoutMarginRight;
mMainLayout.getLayoutParams().width = mScreenWidth;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
mLeftLayout.setTranslationX(-mLeftLayoutWidth);// 最初隐藏mLeftLayout
}
super.onLayout(changed, l, t, r, b);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartPoint.x = (int) event.getX();
mStartPoint.y = (int) event.getY();
// 获取mLeftLayout初始位置
this.location = new int[2];
mLeftLayout.getLocationOnScreen(this.location);
break;
case MotionEvent.ACTION_MOVE:
mStopPoint.x = (int) event.getX();
mStopPoint.y = (int) event.getY();
dx = mStopPoint.x - mStartPoint.x;
int[] location = new int[2];
mLeftLayout.getLocationOnScreen(location);
// 右滑
if (dx > 0) {
if (this.location[0] == -(mLeftLayoutWidth - 1)) {//这里-1 是因为从0开始计算
setViewSlidingWidth(mLeftLayout, dx < mLeftLayoutWidth && location[0] >= -mLeftLayoutWidth ? -mLeftLayoutWidth + dx : 0);
}
} else {// 左滑
if (this.location[0] == 0) {
setViewSlidingWidth(mLeftLayout, -dx < mLeftLayoutWidth ? dx : 0);
}
}
break;
case MotionEvent.ACTION_UP:
int[] loc = new int[2];
mLeftLayout.getLocationOnScreen(loc);
if ((mLeftLayoutWidth + loc[0]) < mLeftLayoutWidth / 2) {// 不到一半
close();
} else {// 大于一半则显示
open();
}
return true;
}
return true;
}
private void setViewSlidingWidth(View view, int width) {
view.setTranslationX(width);
}
public void toggle() {
if (isOpen) {
close();
} else {
open();
}
}
public void open() {
isOpen = true;
setViewSlidingWidth(mLeftLayout, 0);// 显示
}
public void close() {
isOpen = false;
setViewSlidingWidth(mLeftLayout, -mLeftLayoutWidth);// 隐藏
}
}
相信看完上面的代码,读者心里已经比较明白了。但为了功底不是那么扎实的小伙伴,还请容我啰嗦一番。这里继承子RelativeLayout复写了一个ViewGroup,其中只能包含两个子ViewGroup。利用RelativeLayout自身的特点来实现mLeftLayout覆盖在mMainLayout上。然后就是一系列的移动,一定要注意好条件判断。
结束语
自定义View(ViewGroup)是一个很灵活的过程,但是万变不离其宗。重要的不是实现某一个或者某几个自定义View,而是要掌握自定义View的流程,动画和事件拦截机制。然后以此为基础,慢慢才能写出比较高级的View。