android 自定义布局之侧拉布局
侧拉布局是一个自定义的布局,类似QQ的侧拉
代码如下,
package com.example.android_project_two;
/**
* Created by Administrator on 2017/6/21 0021.
*/
import android.content.Context;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.widget.LinearLayout;
/**
* @ 对外仅需设置的接口
*
* -判断左侧布局是否完全显示出来,或完全隐藏,滑动过程中此值无效
* boolean SlidingLayout.isLeftLayoutVisible()
*
* -将屏幕滚动到右侧布局界面
* void SlidingLayout.scrollToRightLayout()
*
* -将屏幕滚动到左侧布局界面
* void SlidingLayout.scrollToLeftLayout()
*
*/
public class SlidingLayout extends LinearLayout {
private static final int SNAP_VELOCITY = 200; //滚动显示和隐藏左侧布局时,手指滑动需要达到的速度。
private VelocityTracker mVelocityTracker; //用于计算手指滑动的速度。
private int touchSlop; //在被判定为滚动之前用户手指可以移动的最大值。
private int screenWidth; //屏幕宽度值
private int leftEdge ; //左边最多可以滑动到的左边缘,值由左边布局的宽度来定,marginLeft到达此值之后,不能再减少。
private int rightEdge = 0; //左边最多可以滑动到的右边缘,值恒为0,即marginLeft到达0之后,不能增加。
private float xDown; //记录手指按下时的横坐标。
private float yDown; //记录手指按下时的纵坐标。
private float xMove; //记录手指移动时的横坐标。
private float yMove; //记录手指移动时的纵坐标。
private float xUp; //记录手机抬起时显示还是隐藏。只有完全显示或隐藏时才会更改此值,滑动过程中此值无效。
private boolean isSliding; //是否正在滑动。的横坐标。
private boolean isLeftLayoutVisible; //左侧布局当前是
private MarginLayoutParams leftLayoutParams; //左侧布局的参数,通过此参数来重新确定左侧布局的宽度,以及更改leftMargin的值。
private MarginLayoutParams rightLayoutParams; //右侧布局的参数,通过此参数来重新确定右侧布局的宽度。
private View leftLayout; //左侧布局对象。
private View rightLayout; //右侧布局对象。
//构造函数
public SlidingLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
//获取屏幕宽度
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
screenWidth = wm.getDefaultDisplay().getWidth();
//获取滑动的最短距离
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
//创建VelocityTracker对象,并将触摸事件加入到VelocityTracker当中。
private void createVelocityTracker(MotionEvent event)
{
if (mVelocityTracker == null)
{
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
//拦截触摸事件
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN:
xDown = event.getRawX();
yDown = event.getRawY();
//当左边菜单完全显示时,点击右边的View,我们希望是拦截,而左边不拦截
if(xDown > leftLayout.getLeft()+leftLayout.getWidth() && isLeftLayoutVisible)
return true;
break;
case MotionEvent.ACTION_MOVE:
int distanceX=(int) (event.getRawX()-xDown);
//水平滑动距离超过一定距离,拦截所有子view的touch事件,来处理自身onTouch事件来达到滑动的目的
if(Math.abs(distanceX)>=touchSlop)
{
return true;
}
break;
}
return false;
}
//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event)
{
// TODO 自动生成的方法存根
createVelocityTracker(event);
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
// 手指按下时,记录按下时的横坐标
xDown = event.getRawX();
yDown = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
// 手指移动时,对比按下时的横坐标,计算出移动的距离,来调整右侧布局的leftMargin值,从而显示和隐藏左侧布局
xMove = event.getRawX();
yMove = event.getRawY();
int distanceX = (int) (xMove - xDown);
int distanceY = (int) (yMove - yDown);
//只有触摸右边的View,才发生滑动
if(xMove > leftLayout.getLeft()+leftLayout.getWidth())
{
//向右滑
if (!isLeftLayoutVisible && distanceX >= touchSlop && (isSliding || Math.abs(distanceY) <= touchSlop))
{
isSliding = true;
leftLayoutParams.leftMargin = leftEdge + distanceX;
if (leftLayoutParams.leftMargin > rightEdge)
{
leftLayoutParams.leftMargin = rightEdge;
}
}
//向左滑
else if (isLeftLayoutVisible && -distanceX >= touchSlop)
{
isSliding = true;
leftLayoutParams.leftMargin = distanceX;
if (leftLayoutParams.leftMargin < leftEdge)
{
leftLayoutParams.leftMargin = leftEdge;
}
}
leftLayout.setLayoutParams(leftLayoutParams);
}
break;
case MotionEvent.ACTION_UP:
xUp = event.getRawX();
int upDistanceX = (int) (xUp - xDown);
if (isSliding)
{
// 手指抬起时,进行判断当前手势的意图,从而决定是滚动到左侧布局,还是滚动到右侧布局
if (wantToShowLeftLayout())
{
if (shouldScrollToLeftLayout())
{
scrollToLeftLayout();
}
else
{
scrollToRightLayout();
}
}
else if (wantToShowRightLayout())
{
if (shouldScrollToRightLayout())
{
scrollToRightLayout();
}
else
{
scrollToLeftLayout();
}
}
}
//在左侧菜单完全显示时,我们希望的是点击右边的View可以发生恢复
else if (upDistanceX < touchSlop && isLeftLayoutVisible && xUp > leftLayout.getLeft()+leftLayout.getWidth())
{
scrollToRightLayout();
}
recycleVelocityTracker();
break;
}
if (this.isEnabled())
{
if (isSliding)
{
unFocusBindView();
return true;
}
if (isLeftLayoutVisible)
{
return true;
}
return false;
}
return true;
}
//将屏幕滚动到左侧布局界面,滚动速度设定为50.
public void scrollToLeftLayout()
{
//传入第一个参数
new ScrollTask().execute(50);
}
//将屏幕滚动到右侧布局界面,滚动速度设定为-50.
public void scrollToRightLayout()
{
//传入第一个参数
new ScrollTask().execute(-50);
}
//左侧布局是否完全显示出来,或完全隐藏,滑动过程中此值无效。
public boolean isLeftLayoutVisible()
{
return isLeftLayoutVisible;
}
//在onLayout中重新设定左侧布局和右侧布局的参数。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
super.onLayout(changed, l, t, r, b);
if (changed)
{
// 获取左侧布局对象
leftLayout = getChildAt(0);
leftLayoutParams = (MarginLayoutParams) leftLayout.getLayoutParams();
// 设置最左边距为负的左侧布局的宽度
leftEdge = -leftLayoutParams.width;
leftLayoutParams.leftMargin = leftEdge;
leftLayout.setLayoutParams(leftLayoutParams);
// 获取右侧布局对象
rightLayout = getChildAt(1);
rightLayoutParams = (MarginLayoutParams) rightLayout.getLayoutParams();
rightLayoutParams.width = screenWidth;
rightLayout.setLayoutParams(rightLayoutParams);
rightLayout.setClickable(true);
}
}
//判断当前手势的意图是不是想显示右侧布局。如果手指移动的距离是负数,且当前左侧布局是可见的,则认为当前手势是想要显示右侧布局。
private boolean wantToShowRightLayout()
{
return xUp - xDown < 0 && isLeftLayoutVisible;
}
//判断当前手势的意图是不是想显示左侧布局。如果手指移动的距离是正数,且当前左侧布局是不可见的,则认为当前手势是想要显示左侧布局。
private boolean wantToShowLeftLayout()
{
return xUp - xDown > 0 && !isLeftLayoutVisible;
}
//判断是否应该滚动将左侧布局展示出来。如果手指移动距离大于屏幕的1/2,或者手指移动速度大于SNAP_VELOCITY,就认为应该滚动将左侧布局展示出来。
private boolean shouldScrollToLeftLayout()
{
return xUp - xDown > leftLayoutParams.width / 2 || getScrollVelocity() > SNAP_VELOCITY;
}
//判断是否应该滚动将右侧布局展示出来。如果手指移动距离加上leftLayoutPadding大于屏幕的1/2,或者手指移动速度大于SNAP_VELOCITY, 就认为应该滚动将右侧布局展示出来。
private boolean shouldScrollToRightLayout()
{
return xDown - xUp > leftLayoutParams.width / 2 || getScrollVelocity() > SNAP_VELOCITY;
}
//获取手指在右侧布局的监听View上的滑动速度。
private int getScrollVelocity()
{
mVelocityTracker.computeCurrentVelocity(1000);
int velocity = (int) mVelocityTracker.getXVelocity();
return Math.abs(velocity);
}
//回收VelocityTracker对象。
private void recycleVelocityTracker()
{
mVelocityTracker.recycle();
mVelocityTracker = null;
}
//使用可以获得焦点的控件在滑动的时候失去焦点。
private void unFocusBindView()
{
if (rightLayout != null)
{
rightLayout.setPressed(false);
rightLayout.setFocusable(false);
rightLayout.setFocusableInTouchMode(false);
}
}
//*********************************************************************************************************************
/**
* @ 利用轻量级线程实现滑动 ,应用在手指松开的滑动动画
*/
class ScrollTask extends AsyncTask<Integer, Integer, Integer>
{
//后台处理,入口参数对应第一个参数类型
@Override
protected Integer doInBackground(Integer... speed) {
int leftMargin = leftLayoutParams.leftMargin;
// 根据传入的速度来滚动界面,当滚动到达左边界或右边界时,跳出循环。
while (true)
{
leftMargin = leftMargin + speed[0];
if (leftMargin > rightEdge)
{
leftMargin = rightEdge;
break;
}
if (leftMargin < leftEdge)
{
leftMargin = leftEdge;
break;
}
//主动回调onProgressUpdate来更新界面(滑动)
publishProgress(leftMargin);
//使当前线程睡眠指定的毫秒数,为了要有滚动效果产生,每次循环使线程睡眠10毫秒,这样肉眼才能够看到滚动动画。
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
if (speed[0] > 0)
{
isLeftLayoutVisible = true;
}
else
{
isLeftLayoutVisible = false;
}
isSliding = false;
//返回对应第三个参数类型,并且把值传入onPostExecute
return leftMargin;
}
//调用publishProgress时,回调这个方法,用来更新界面(滑动),入口参数对应第二个参数类型
@Override
protected void onProgressUpdate(Integer... leftMargin)
{
leftLayoutParams.leftMargin = leftMargin[0];
leftLayout.setLayoutParams(leftLayoutParams);
//使用可以获得焦点的控件在滑动的时候失去焦点。
unFocusBindView();
}
//在doInBackground执行完成后执行界面更新,入口参数对应第三个参数类型
@Override
protected void onPostExecute(Integer leftMargin)
{
leftLayoutParams.leftMargin = leftMargin;
leftLayout.setLayoutParams(leftLayoutParams);
}
}
}
在xml方面的布局就直接引用它就行,这个代码注释也是很多,很容易懂