說(shuō)到滑動(dòng)解鎖,就回到了2012~2014年,iPhone4S、5、5S年代,如今準(zhǔn)備踏入2020年,這些年國(guó)產(chǎn)機(jī)崛起,再也不是公交車(chē)上都是iPhone4S的場(chǎng)景。本篇來(lái)使用ViewDragHelper實(shí)現(xiàn)滑動(dòng)解鎖。
成品展示

先來(lái)分析一下頁(yè)面的元素
- 背景圖
- 圓角滑道
- 圓形滑塊
- 閃動(dòng)提示文字
其他一些細(xì)節(jié):
- 滑道和圓形滑塊之間有些邊距,我們使用padding來(lái)處理。
我們需要自定義的就是第2點(diǎn),這個(gè)滑道包含一個(gè)滑塊的圖片和提示文字,滑塊使用原生ImageView即可,而提示文字則是一個(gè)支持漸變著色的TextView(不是重點(diǎn))。
漸變著色的TextView
先秒掉簡(jiǎn)單的,漸變著色的TextView,不是重點(diǎn),代碼量不多。
public class ShineTextView extends TextView {
private LinearGradient mLinearGradient;
private Matrix mGradientMatrix;
private int mViewWidth = 0;
private int mTranslate = 0;
public ShineTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mViewWidth == 0) {
mViewWidth = getMeasuredWidth();
if (mViewWidth > 0) {
Paint paint = getPaint();
mLinearGradient = new LinearGradient(0,
0,
mViewWidth,
0,
new int[]{getCurrentTextColor(), 0xffffffff, getCurrentTextColor()},
null,
Shader.TileMode.CLAMP);
paint.setShader(mLinearGradient);
mGradientMatrix = new Matrix();
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mGradientMatrix != null) {
mTranslate += mViewWidth / 5;
if (mTranslate > 2 * mViewWidth) {
mTranslate = -mViewWidth;
}
mGradientMatrix.setTranslate(mTranslate, 0);
mLinearGradient.setLocalMatrix(mGradientMatrix);
//每80毫秒執(zhí)行onDraw()
postInvalidateDelayed(80);
}
}
}
下面重點(diǎn)介紹我們使用ViewDragHelper實(shí)現(xiàn)拽托、滑動(dòng)的滑道View:SlideLockView
讓滑塊滑動(dòng)起來(lái)
滑道實(shí)際就是一個(gè)FrameLayout,我們使用ViewDragHelper將滑塊ImageView進(jìn)行拽托,主要我們要做以下幾件事:
- 限制拽托的左側(cè)起點(diǎn)、右側(cè)終點(diǎn)(否則滑塊就出去啦!)
- 松手時(shí)判斷滑塊的x坐標(biāo)是偏向滑道的左側(cè)還是右側(cè),來(lái)決定滑動(dòng)到起點(diǎn)還是終點(diǎn)。
- 滾動(dòng)結(jié)束,判斷是否到達(dá)了右側(cè)的終點(diǎn)。
- 判斷拽托速度,如果超過(guò)指定速度,則自動(dòng)滾動(dòng)滑塊到右側(cè)終點(diǎn)。
看到這4點(diǎn),如果讓我們用事件分發(fā)來(lái)處理,代碼量和判斷會(huì)非常多,并且需要做速度檢測(cè),而使用ViewDragHelper,上面4點(diǎn)都封裝好啦,我們添加一個(gè)回調(diào),再將事件委托給它,在回調(diào)中做事情上面4點(diǎn)的處理,一切都簡(jiǎn)單起來(lái)了。
- 創(chuàng)建SlideLockView,繼承FrameLayout
public class SlideLockView extends FrameLayout {
/**
* 拽托幫助類(lèi)
*/
private ViewDragHelper mViewDragHelper;
public SlideLockView(@NonNull Context context) {
this(context, null);
}
public SlideLockView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideLockView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
//進(jìn)行初始化...
}
}
- 創(chuàng)建ViewDragHelper,使用create靜態(tài)方法創(chuàng)建,有3個(gè)參數(shù),第一個(gè)拽托控件的父控件(就是當(dāng)前View),第二個(gè)參數(shù)是拽托靈敏度,數(shù)值越大,越靈敏,默認(rèn)為1.0,第三個(gè)參數(shù)為回調(diào)對(duì)象。
private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
final SlideLockView slideRail = this;
mViewDragHelper = ViewDragHelper.create(this, 0.3f, new ViewDragHelper.Callback() {
...
});
}
- 委托onInterceptTouchEvent、onTouchEvent事件給ViewDragHelper
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//將onInterceptTouchEvent委托給ViewDragHelper
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//將onTouchEvent委托給ViewDragHelper
mViewDragHelper.processTouchEvent(event);
return true;
}
- 找到布局中的滑塊,我們要求滑塊的id為lock_btn,所以需要在ids.xml中預(yù)先定義這個(gè)id,如果沒(méi)有查找到,則拋出異常。
//文件名:ids.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="lock_btn" type="id" />
</resources>
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//找到需要拽托的滑塊
mLockBtn = findViewById(R.id.lock_btn);
if (mLockBtn == null) {
throw new NullPointerException("必須要有一個(gè)滑動(dòng)滑塊");
}
}
- 剩下的事情就在ViewDragHelper的回調(diào)中設(shè)置。
復(fù)寫(xiě)tryCaptureView()、clampViewPositionHorizontal()、clampViewPositionVertical()。
- tryCaptureView為判斷子View是否可以拽托
- clampViewPositionHorizontal()則是橫向拽托子View時(shí)回調(diào),返回可以拽托到的位置。
- clampViewPositionVertical則是縱向拽托。
private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
final SlideLockView slideRail = this;
mViewDragHelper = ViewDragHelper.create(this, 0.3f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
//判斷能拽托的View,這里會(huì)遍歷內(nèi)部子控件來(lái)決定是否可以拽托,我們只需要滑塊可以滑動(dòng)
return child == mLockBtn;
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
//拽托子View橫向滑動(dòng)時(shí)回調(diào),回調(diào)的left,則是可以滑動(dòng)的左上角x坐標(biāo)
return left;
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
//拽托子View縱向滑動(dòng)時(shí)回調(diào),鎖定頂部padding距離即可,不能不復(fù)寫(xiě),否則少了頂部的padding,位置就偏去上面了
return getPaddingTop();
}
});
}
限制滑塊滑動(dòng)范圍
經(jīng)過(guò)上面3個(gè)方法重寫(xiě),滑塊已經(jīng)可以左右滑動(dòng)了,但是可以滑動(dòng)出滑道(父控件),我們需要限制橫向滑動(dòng)的范圍,不能超過(guò)左側(cè)起點(diǎn)和右側(cè)終點(diǎn)。我們需要修改clampViewPositionHorizontal這個(gè)方法。
左側(cè)起點(diǎn)的x坐標(biāo),就是paddingStart。
右側(cè)終點(diǎn),為滑道總長(zhǎng)度 - 右邊邊距 - 滑塊寬度。
判斷回調(diào)的left值,如果小于起點(diǎn),則強(qiáng)制為起點(diǎn),如果大于右側(cè)終點(diǎn)值,則強(qiáng)制為終點(diǎn)。
這樣處理,滑塊則不會(huì)滑出滑道了!代碼量不對(duì),也很清晰。
private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
final SlideLockView slideRail = this;
mViewDragHelper = ViewDragHelper.create(this, 0.3f, new ViewDragHelper.Callback() {
//...
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
//拽托子View橫向滑動(dòng)時(shí)回調(diào),回調(diào)的left,則是可以滑動(dòng)的左上角x坐標(biāo)
int lockBtnWidth = mLockBtn.getWidth();
//限制左右臨界點(diǎn)
int fullWidth = slideRail.getWidth();
//最少的左邊
int leftMinDistance = getPaddingStart();
//最多的右邊
int leftMaxDistance = fullWidth - getPaddingEnd() - lockBtnWidth;
//修復(fù)兩端的臨界值
if (left < leftMinDistance) {
return leftMinDistance;
} else if (left > leftMaxDistance) {
return leftMaxDistance;
}
return left;
}
//...
});
}
松手回彈和速度檢測(cè)
有了滑動(dòng)和限制滑動(dòng)范圍,我們還有一個(gè)松手回彈和速度檢測(cè),ViewDragHelper同樣給我們封裝了,提供了一個(gè)onViewReleased()回調(diào),并且做了速度檢測(cè),將速度也回傳給了我們。
- 復(fù)寫(xiě)onViewCaptured(),主要是為了獲取一開(kāi)始捕獲到滑塊時(shí),他的top值。
- 復(fù)寫(xiě)onViewReleased(),主要是計(jì)算松手時(shí),滑塊比較近起點(diǎn)還比較近是終點(diǎn),使用ViewDragHelper的settleCapturedViewAt()方法,開(kāi)始彈性滾動(dòng)滑塊去到起點(diǎn)或終點(diǎn)。
- 判斷中,我們添加判斷速度是否超過(guò)1000,如果超過(guò),即使拽托距離比較小,就當(dāng)為fling操作,讓滑塊滾動(dòng)到終點(diǎn)。
settleCapturedViewAt()這個(gè)方法,內(nèi)部是使用Scroller進(jìn)行彈性滾動(dòng)的,所以我們需要復(fù)寫(xiě)父View的computeScroll()方法,進(jìn)行內(nèi)容滾動(dòng)處理。
如果不知道為什么這么做,搜索一下Scroller的資料了解一下~
private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
final SlideLockView slideRail = this;
mViewDragHelper = ViewDragHelper.create(this, 0.3f, new ViewDragHelper.Callback() {
private int mTop;
//...
@Override
public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
//捕獲到拽托的View時(shí)回調(diào),獲取頂部距離
mTop = capturedChild.getTop();
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//獲取滑塊當(dāng)前的位置
int currentLeft = releasedChild.getLeft();
//獲取滑塊的寬度
int lockBtnWidth = mLockBtn.getWidth();
//獲取滑道寬度
int fullWidth = slideRail.getWidth();
//一般滑道的寬度,用來(lái)判斷滑塊距離起點(diǎn)近還是終點(diǎn)近
int halfWidth = fullWidth / 2;
//松手位置在小于一半,并且滑動(dòng)速度小于1000,則回到左邊
if (currentLeft <= halfWidth && xvel < 1000) {
mViewDragHelper.settleCapturedViewAt(getPaddingStart(), mTop);
} else {
//否則去到右邊(寬度,減去padding和滑塊寬度)
mViewDragHelper.settleCapturedViewAt(fullWidth - getPaddingEnd() - lockBtnWidth, mTop);
}
invalidate();
}
});
}
@Override
public void computeScroll() {
super.computeScroll();
//判斷是否移動(dòng)到頭了,未到頭則繼續(xù)
if (mViewDragHelper != null) {
if (mViewDragHelper.continueSettling(true)) {
invalidate();
}
}
}
解鎖回調(diào)
經(jīng)過(guò)上面的編碼,滑動(dòng)解鎖就完成了,但還差一個(gè)解鎖回調(diào),進(jìn)行解鎖操作,并且我們需要一個(gè)時(shí)機(jī)知道滾動(dòng)結(jié)束了(ViewDragHelper狀態(tài)回調(diào),滾動(dòng)閑置了,并且滑塊位于終點(diǎn),則為解鎖完成)。
-
復(fù)寫(xiě)onViewDragStateChanged()方法,處理ViewDragHelper狀態(tài)改變,狀態(tài)主要有以下3個(gè):
- STATE_IDLE = 0,滾動(dòng)閑置,可以認(rèn)為滾動(dòng)停止了。
- STATE_DRAGGING = 1,正在拽托。
- STATE_SETTLING = 2,fling操作時(shí)。
提供Callback接口回調(diào)和設(shè)置方法。
我們?cè)趏nViewDragStateChanged()回調(diào)中判斷,狀態(tài)為STATE_IDLE,并且滑塊位置為終點(diǎn)值時(shí),就為解鎖,并且回調(diào)Callback對(duì)象。
public class SlideLockView extends FrameLayout {
/**
* 回調(diào)
*/
private Callback mCallback;
/**
* 是否解鎖
*/
private boolean isUnlock = false;
private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
final SlideLockView slideRail = this;
mViewDragHelper = ViewDragHelper.create(this, 0.3f, new ViewDragHelper.Callback() {
private int mTop;
//...
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
int lockBtnWidth = mLockBtn.getWidth();
//限制左右臨界點(diǎn)
int fullWidth = slideRail.getWidth();
//最多的右邊
int leftMaxDistance = fullWidth - getPaddingEnd() - lockBtnWidth;
int left = mLockBtn.getLeft();
if (state == ViewDragHelper.STATE_IDLE) {
//移動(dòng)到最右邊,解鎖完成
if (left == leftMaxDistance) {
//未解鎖才進(jìn)行解鎖回調(diào),由于這個(gè)判斷會(huì)進(jìn)兩次,所以做了標(biāo)志位限制
if (!isUnlock) {
isUnlock = true;
if (mCallback != null) {
mCallback.onUnlock();
}
}
}
}
}
});
}
public interface Callback {
/**
* 當(dāng)解鎖時(shí)回調(diào)
*/
void onUnlock();
}
public void setCallback(Callback callback) {
mCallback = callback;
}
}
完整代碼
public class SlideLockView extends FrameLayout {
/**
* 滑動(dòng)滑塊
*/
private View mLockBtn;
/**
* 拽托幫助類(lèi)
*/
private ViewDragHelper mViewDragHelper;
/**
* 回調(diào)
*/
private Callback mCallback;
/**
* 是否解鎖
*/
private boolean isUnlock = false;
public SlideLockView(@NonNull Context context) {
this(context, null);
}
public SlideLockView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideLockView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
final SlideLockView slideRail = this;
mViewDragHelper = ViewDragHelper.create(this, 0.3f, new ViewDragHelper.Callback() {
private int mTop;
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
//判斷能拽托的View,這里會(huì)遍歷內(nèi)部子控件來(lái)決定是否可以拽托,我們只需要滑塊可以滑動(dòng)
return child == mLockBtn;
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
//拽托子View橫向滑動(dòng)時(shí)回調(diào),回調(diào)的left,則是可以滑動(dòng)的左上角x坐標(biāo)
int lockBtnWidth = mLockBtn.getWidth();
//限制左右臨界點(diǎn)
int fullWidth = slideRail.getWidth();
//最少的左邊
int leftMinDistance = getPaddingStart();
//最多的右邊
int leftMaxDistance = fullWidth - getPaddingEnd() - lockBtnWidth;
//修復(fù)兩端的臨界值
if (left < leftMinDistance) {
return leftMinDistance;
} else if (left > leftMaxDistance) {
return leftMaxDistance;
}
return left;
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
//拽托子View縱向滑動(dòng)時(shí)回調(diào),鎖定頂部padding距離即可,不能不復(fù)寫(xiě),否則少了頂部的padding,位置就偏去上面了
return getPaddingTop();
}
@Override
public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
//捕獲到拽托的View時(shí)回調(diào),獲取頂部距離
mTop = capturedChild.getTop();
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//獲取滑塊當(dāng)前的位置
int currentLeft = releasedChild.getLeft();
//獲取滑塊的寬度
int lockBtnWidth = mLockBtn.getWidth();
//獲取滑道寬度
int fullWidth = slideRail.getWidth();
//一般滑道的寬度,用來(lái)判斷滑塊距離起點(diǎn)近還是終點(diǎn)近
int halfWidth = fullWidth / 2;
//松手位置在小于一半,并且滑動(dòng)速度小于1000,則回到左邊
if (currentLeft <= halfWidth && xvel < 1000) {
mViewDragHelper.settleCapturedViewAt(getPaddingStart(), mTop);
} else {
//否則去到右邊(寬度,減去padding和滑塊寬度)
mViewDragHelper.settleCapturedViewAt(fullWidth - getPaddingEnd() - lockBtnWidth, mTop);
}
invalidate();
}
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
int lockBtnWidth = mLockBtn.getWidth();
//限制左右臨界點(diǎn)
int fullWidth = slideRail.getWidth();
//最多的右邊
int leftMaxDistance = fullWidth - getPaddingEnd() - lockBtnWidth;
int left = mLockBtn.getLeft();
if (state == ViewDragHelper.STATE_IDLE) {
//移動(dòng)到最右邊,解鎖完成
if (left == leftMaxDistance) {
//未解鎖才進(jìn)行解鎖回調(diào),由于這個(gè)判斷會(huì)進(jìn)兩次,所以做了標(biāo)志位限制
if (!isUnlock) {
isUnlock = true;
if (mCallback != null) {
mCallback.onUnlock();
}
}
}
}
}
});
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//找到需要拽托的滑塊
mLockBtn = findViewById(R.id.lock_btn);
if (mLockBtn == null) {
throw new NullPointerException("必須要有一個(gè)滑動(dòng)滑塊");
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//將onInterceptTouchEvent委托給ViewDragHelper
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//將onTouchEvent委托給ViewDragHelper
mViewDragHelper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
//判斷是否移動(dòng)到頭了,未到頭則繼續(xù)
if (mViewDragHelper != null) {
if (mViewDragHelper.continueSettling(true)) {
invalidate();
}
}
}
public interface Callback {
/**
* 當(dāng)解鎖時(shí)回調(diào)
*/
void onUnlock();
}
public void setCallback(Callback callback) {
mCallback = callback;
}
}
基本使用
- Xml控件布局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
android:background="@drawable/app_lock_screen_bg"
tools:context=".ScreenLockActivity">
<com.zh.android.slidelockscreen.widget.SlideLockView
android:id="@+id/slide_rail"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="20dp"
android:background="@drawable/app_slide_rail_bg"
android:paddingStart="6dp"
android:paddingTop="8dp"
android:paddingEnd="6dp"
android:paddingBottom="8dp">
<com.zh.android.slidelockscreen.widget.ShineTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:gravity="center"
android:text="右滑解鎖打開(kāi)應(yīng)用"
android:textColor="@color/app_tip_text"
android:textSize="18sp" />
<ImageView
android:id="@id/lock_btn"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/app_lock_btn" />
</com.zh.android.slidelockscreen.widget.SlideLockView>
</FrameLayout>
- Java代碼
public class ScreenLockActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_screen_lock);
SlideLockView slideRail = findViewById(R.id.slide_rail);
slideRail.setCallback(new SlideLockView.Callback() {
@Override
public void onUnlock() {
//解鎖,跳轉(zhuǎn)到首頁(yè)
Intent intent = new Intent(ScreenLockActivity.this, HomeActivity.class);
startActivity(intent);
finish();
}
});
}
}