本篇文章我们来实现一个带有弹性滑动效果的自定义View。当然,文章的侧重点是自定义View但也会详细讲解Android中View的事件分发体系,另外还会涉及到一些其他方面的知识,比如如何实现带有阻尼效果的弹性滑动。我可以保证,看完这篇文章你一定回对自定义View以及事件分发有一个更深的理解!还是老规矩,看下最终实现效果。
分析图中效果会发现其核心功能类似于一个简单的下拉刷新、上拉加载的框架,但又有很大的区别。因此我们还是先来罗列一下几个实现步骤,如下:
一. 明确需求,确定对外开放的接口
二. 分析滑动效果,初步实现控件布局
三.关于滑动,不得不说的事件分发
一. 明确需求,确定对外开放接口
我们应该明确控件的需求,确定有哪些功能,然后做针对性开发。在这里我们就先贴出如何使用控件,也是为了更好地认识控件的需求。
1.布局文件添加
<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=".MainActivity">
<com.zhpan.lockview.view.LockView
android:id="@+id/lock_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
2.设置操作的监听事件。代码如下:
mLockView.setOnLockOperateListener(new OnLockOperateListener() {
@Override
public void onLockPrepared() {// 上锁就绪
}
@Override
public void onUnLockPrepared() {// 开锁就绪
}
@Override
public void onLockStart() {// 开始上锁
}
@Override
public void onUnlockStart() {// 开始开锁
}
@Override
public void onNotPrepared() {// 上下滑动距离未达到就绪状态
}
});
3.对外开放接口
// 设置蓝牙是否连接
mLockView.setBluetoothConnect(false);
// 设置上锁状态
mLockView.setLockState(isLock);
// 设置View是否可以滑动
mLockView.setCanSlide(true)
// 设置滑动阻尼大小
mLockView.setDamping(1.7)
// 设置View中心文字
mLockView.setText("已上锁");
// 设置中心大圆的颜色
mLockView.setCircleColor
// 开启心跳动画
mLockView.startWave();
// 停止心跳动画
mLockView.stopWave();
// 是否正在搜索/连接蓝牙
mLockView.connecting(true);
// 点击事件监听(只有在未连接蓝牙时有效)
mLockView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
我们来总结下控件中需要实现的功能:
- 控件布局的实现。
- 蓝牙未连接时,只能点击而不能滑动。
- 点击事件以及连接中动画。
- 更改连接状态。
- 实现上下弹性滑动,且需要控制滑动边界。
- 滑动事件回掉。
- 心跳动画实现。
以上几点就是我们要完成的核心功能,有了需求之后就直接进入主题来实现我们想要的效果吧。
二、分析控件,初步实现控件布局
分析上图的效果我们发现,中间的View是可滑动的,且覆盖在上下小圆点的上面。因此我们发现这种效果直接继承View实现起来会不太方便。因此我们可以想到利用自定义ViewGroup来布局页面。这么一来使我们的开发简单了许多。因此我们先新建一个layout_oval_lock.xml的布局为文件,理所当然我们需要用FrameLayout来布局控件,这样就实现了层次叠加效果,FrameLayout内部是两个自定义View,我们可以暂且搁置不管,后面会讲到如何实现。布局文件如下:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<com.zhpan.lockview.view.CircleView
android:id="@+id/green_cv"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginTop="110dp"
app:circle_color="@color/green"
android:layout_gravity="center"/>
<com.zhpan.lockview.view.CircleView
android:id="@+id/red_cv"
android:layout_width="15dp"
android:layout_height="15dp"
app:circle_color="@color/red"
android:layout_marginTop="-110dp"
android:layout_gravity="center"/>
<com.zhpan.lockview.view.CircleWaveView
android:id="@+id/circle_wave_view"
android:layout_width="220dp"
android:layout_height="300dp"
android:layout_gravity="center"
android:padding="20dp"/>
<ProgressBar
android:id="@+id/progress"
android:layout_width="30dp"
android:layout_height="30dp"
android:visibility="gone"
android:indeterminateTint="@color/white"
android:layout_gravity="center"/>
</FrameLayout>
接下来新建一个LockView类并继承FrameLayout。LockView与 上边layout_oval_lock的布局文件关联,并重写相应的方法,同样我们还可以在这里去为控件添加一些Attribute,方便在布局文件中使用,但这个控件中我并没有去定义属性,因此这里也不再讲解。代码如下:
public LockView(Context context) {
this(context, null);
}
public LockView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LockView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
View view = View.inflate(context, R.layout.layout_oval_lock, this);
mCircleWaveView = (CircleWaveView) view.findViewById(R.id.circle_wave_view);
mCircleView = (CircleView) view.findViewById(R.id.green_cv);
distance = ((LayoutParams) mCircleView.getLayoutParams()).topMargin;
mProgressBar = (ProgressBar) view.findViewById(R.id.progress);
mScroller = mCircleWaveView.getScroller();
mContext = context;
mCircleWaveView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
}
});
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
View view = getChildAt(0);
view.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
三.关于滑动,不得不说的事件分发
接下来就要来处理中心View的滑动了!说到滑动,避免不了的就应该想到Android中View的事件分发体系,那么对于滑动事件的处理我们需要对重写三个方法。我想很多小伙伴肯定已经想到了!没错,就是事件分发的三个核心方法:dispatchTouchEvent、onInterceptTouchEvent、以及onTouchEvent。我觉得还是先简单来了解一下这三个方法吧,因为它确实还是挺重要的。
- dispatchTouchEvent 顾名思义,这个方法就是用来对事件进行分发的。如果事件传递到了当前View,那么这个方法一定会被调用。它的返回结果受当前View的onTouchEvent和下级View的onInterceptTouchEvent方法的影响,表示是否消费当前事件。需要注意的是View并没有该方法,这个方法仅仅存在于ViewGroup中!
- onInterceptTouchEvent 这个方法在dispatchTouchEvent方法的内部被调用,用来表示是否拦截某个事件。返回结果表示是否拦截当前事件。
- onTouchEvent 这个方法在dispatchTouchEvent方法中调用。用来处理点击事件。返回结果表示是否消费当前事件。
首先来看dispatchTouchEvent方法中的代码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!canSlide)
switch (ev.getAction()) {
case ACTION_DOWN:
timestamp = System.currentTimeMillis();
break;
case ACTION_UP:
if (System.currentTimeMillis() - timestamp < 500) {
performClick();
return true;
}
break;
}
return super.dispatchTouchEvent(ev);
}
上面我们提到,只要有事件传递到当前的ViewGroup那么dispatchTouchEvent就会首先被调用!因此在这个方法里我们先来判断当前是否是可以滑动状态(蓝牙未连接时不可滑动)。如果不可以滑动,那么就去我们去处理点击事件,我们认为ACTION_DOWN和ACTION_UP之间间隔小于500毫秒就是一次点击事件,那么我们就在此处调用performClick方法并消费掉当前事件,如果间隔大于500毫秒,我们认为不是点击事件,那么紧接着就去调用父类的dispatchTouchEvent方法。如果当前可以滑动,那么我们同样调用父类的dispatchTouchEvent方法来处理。
接下来我们看onInterceptTouchEvent方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = true;
int y = (int) ev.getY();
switch (ev.getAction()) {
case ACTION_DOWN:
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(y - mLastY) > mTouchSlop) {
intercepted = true;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastY = y;
return intercepted;
}
在这个方法中我们重点来看ACTION_MOVE的时候,在这里先判断了滑动的距离是否大于mTouchSlop,这个值是认为滑动的最小距离,当大于这个值的时候就认为是滑动了。那么看intercepted返回了true,表示要拦截这个事件!那么那么拦截了这个滑动事件会怎么样呢?答案是当前View中的onTouchEvent方法被调用了!那么请将我们的目光聚焦到onTouchEvent方法中,注意前方高能!
核心中最核心的onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
int scrollY = mCircleWaveView.getScrollY();
switch (event.getAction()) {
case ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
if (!canSlide) {
return super.onTouchEvent(event);
}
int deltaY = (int) ((mLastY - y) / damping);
if (mCircleWaveView.getScrollY() > mTouchSlop) {
mOption = Option.LOCK;
} else if (mCircleWaveView.getScrollY() < -mTouchSlop) {
mOption = Option.UNLOCK;
}
if (Math.abs(scrollY) > (distance - mCircleWaveView.getRadius() + mCircleView.getRadius())) {
if (mOption != null) {
switch (mOption) {
case LOCK:
if (mOnLockOperateListener != null)
mOnLockOperateListener.onLockPrepared();
mCircleWaveView.setLockPrepared(true);
break;
case UNLOCK:
if (mOnLockOperateListener != null)
mOnLockOperateListener.onUnLockPrepared();
mCircleWaveView.setUnLockPrePared(true);
break;
}
}
} else {
mCircleWaveView.setUnLockPrePared(false);
mCircleWaveView.setLockPrepared(false);
mOnLockOperateListener.onNotPrepared();
/* if (isLock()) {
mCircleWaveView.setText(mContext.getResources().getString(R.string.device_control_unlock));
} else {
mCircleWaveView.setText(mContext.getResources().getString(R.string.device_control_lock));
}*/
// isOperating = false;
}
/**
* 控制滑动边界
*/
int border = (distance - mCircleWaveView.getRadius() + mCircleView.getRadius()) +
DensityUtils.dp2px(mContext, 25);// 可上下滑动的最大距离
// 当前上下滑动的距离
int slideHeight = deltaY + mCircleWaveView.getScrollY();
if (slideHeight > border) {
mCircleWaveView.scrollTo(0, border);
return true;
} else if (slideHeight + border < 0) {
mCircleWaveView.scrollTo(0, -border);
return true;
}
mCircleWaveView.scrollBy(0, deltaY);
break;
case MotionEvent.ACTION_UP:
mCircleWaveView.setUnLockPrePared(false);
mCircleWaveView.setLockPrepared(false);
scrollY = mCircleWaveView.getScrollY();
if (Math.abs(scrollY) > (distance - mCircleWaveView.getRadius() + mCircleView.getRadius()) && mOption != null) {
switch (mOption) {
case LOCK:
if (mOnLockOperateListener != null)
mOnLockOperateListener.onLockStart();
break;
case UNLOCK:
if (mOnLockOperateListener != null)
mOnLockOperateListener.onUnlockStart();
break;
}
}
mCircleWaveView.smoothScroll(0, 0);
break;
}
mLastY = y;
return super.onTouchEvent(event);
}
看到这个方法中这么多代码不知道各位是否已经懵逼(好吧,我承认,这地方代码写的确实比较乱)?哈哈,不过没关系,其实细细分析来还是不难理解的!同样,我们选择比较重要的点来看。首先来看ACTION_MOVE的时候,在这里先判断了是否可以滑动(其实不可以滑动的情况下应该不会走到这个方法,但是为了严谨还是加了判断),如果不能滑动则下边的逻辑全都不会再走了。接下来
通过判断滑动的方向来确定是要开锁还是关锁,并根据滑动距离来给出回调处理,下边贴一下回调接口:
public interface OnLockOperateListener {
// 上锁就绪
void onLockPrepared();
// 开锁就绪
void onUnLockPrepared();
// 开始上锁
void onLockStart();
// 开始开锁
void onUnlockStart();
// 未就绪
void onNotPrepared();
}
一共五个方法,注释写的清楚这里不再解释。然后是通过一系列计算来控制View的滑动边界,具体看代码也不再分析。最后一点是控制View的滑动,关于弹性滑动有必要说一下。这里我们给View加了一个弹性滑动和阻尼效果。其中弹性滑动是在CircleWaveView中通过Scroller来实现的,CircleWaveView暴漏出来smoothScroll的弹性滑动接口供这里调用。这点我们在后面讲解CircleWaveView会再讲到。而阻尼滑动则是将原滑动距离除以阻尼系数以减小滑动距离从而产生阻尼效果。接下来在ACTION_UP中,同样是根据滑动距离给出对应的回调,并将View恢复到原位。
到这里本篇文章的内容基本就已经结束了,接下来我们要做的是一些修补工作,比如对外暴漏出一些接口,提供上层调用。
小结
本篇文章着重讲解了自定义LockView的弹性滑动实现,并且探讨了关于事件分发的一些知识。相信各位小伙伴如果能认真看完一定会对自定义View以及事件分发有一个更深刻的认识。最后,关于自定义LockView的东西其实还未讲完,但由于文章篇幅太长,我打算再下一篇再做分析,本篇文章到此结束。