目錄
效果展示
邏輯解析
其實整個效果邏輯非常的簡單萍倡,首先當(dāng)整個控件是覆蓋全屏的情況時靴寂,我們拖動向下滑動超過一定的范圍的時候它就自動的滑動到下面否則就回彈
而當(dāng)控件的狀態(tài)是展開狀態(tài)的時候,手指向上滑動超過一定的距離的時候就自動恢復(fù)到原始狀態(tài)
代碼實現(xiàn)
1.ViewDragHelper的創(chuàng)建方法
這里我們使用Android本身提供的一個非常好用的工具ViewDragHelper它可以非常方便的實現(xiàn)拖動的效果,它的創(chuàng)建函數(shù)如下所示(摘自源碼):
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
public static ViewDragHelper create(@NonNull ViewGroup forParent, float sensitivity,
@NonNull Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
我們可以看到,這里需要三個參數(shù):
forParent:需要子控件實現(xiàn)拖動效果的ViewGroup,這里我們自定義的就是ViewGroup因此傳當(dāng)前控件的對象即可
sensitivity:它是滑動的敏感度类咧,一般傳個1就行
cb:這個比較重要,是拖動過程中的回調(diào)蟹腾,因此我們大部分的操作都在這里面提供的回調(diào)方法
它的所有回調(diào)方法如下所示:
public abstract static class Callback {
/**
* Called when the drag state changes. See the <code>STATE_*</code> constants
* for more information.
*
* @param state The new drag state
*
* @see #STATE_IDLE
* @see #STATE_DRAGGING
* @see #STATE_SETTLING
*/
public void onViewDragStateChanged(int state) {}
/**
* Called when the captured view's position changes as the result of a drag or settle.
*
* @param changedView View whose position changed
* @param left New X coordinate of the left edge of the view
* @param top New Y coordinate of the top edge of the view
* @param dx Change in X position from the last call
* @param dy Change in Y position from the last call
*/
public void onViewPositionChanged(@NonNull View changedView, int left, int top, @Px int dx,
@Px int dy) {
}
/**
* Called when a child view is captured for dragging or settling. The ID of the pointer
* currently dragging the captured view is supplied. If activePointerId is
* identified as {@link #INVALID_POINTER} the capture is programmatic instead of
* pointer-initiated.
*
* @param capturedChild Child view that was captured
* @param activePointerId Pointer id tracking the child capture
*/
public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {}
/**
* Called when the child view is no longer being actively dragged.
* The fling velocity is also supplied, if relevant. The velocity values may
* be clamped to system minimums or maximums.
*
* <p>Calling code may decide to fling or otherwise release the view to let it
* settle into place. It should do so using {@link #settleCapturedViewAt(int, int)}
* or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes
* one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING}
* and the view capture will not fully end until it comes to a complete stop.
* If neither of these methods is invoked before <code>onViewReleased</code> returns,
* the view will stop in place and the ViewDragHelper will return to
* {@link #STATE_IDLE}.</p>
*
* @param releasedChild The captured child view now being released
* @param xvel X velocity of the pointer as it left the screen in pixels per second.
* @param yvel Y velocity of the pointer as it left the screen in pixels per second.
*/
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {}
/**
* Called when one of the subscribed edges in the parent view has been touched
* by the user while no child view is currently captured.
*
* @param edgeFlags A combination of edge flags describing the edge(s) currently touched
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void onEdgeTouched(int edgeFlags, int pointerId) {}
/**
* Called when the given edge may become locked. This can happen if an edge drag
* was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)}
* was called. This method should return true to lock this edge or false to leave it
* unlocked. The default behavior is to leave edges unlocked.
*
* @param edgeFlags A combination of edge flags describing the edge(s) locked
* @return true to lock the edge, false to leave it unlocked
*/
public boolean onEdgeLock(int edgeFlags) {
return false;
}
/**
* Called when the user has started a deliberate drag away from one
* of the subscribed edges in the parent view while no child view is currently captured.
*
* @param edgeFlags A combination of edge flags describing the edge(s) dragged
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
/**
* Called to determine the Z-order of child views.
*
* @param index the ordered position to query for
* @return index of the view that should be ordered at position <code>index</code>
*/
public int getOrderedChildIndex(int index) {
return index;
}
/**
* Return the magnitude of a draggable child view's horizontal range of motion in pixels.
* This method should return 0 for views that cannot move horizontally.
*
* @param child Child view to check
* @return range of horizontal motion in pixels
*/
public int getViewHorizontalDragRange(@NonNull View child) {
return 0;
}
/**
* Return the magnitude of a draggable child view's vertical range of motion in pixels.
* This method should return 0 for views that cannot move vertically.
*
* @param child Child view to check
* @return range of vertical motion in pixels
*/
public int getViewVerticalDragRange(@NonNull View child) {
return 0;
}
/**
* Called when the user's input indicates that they want to capture the given child view
* with the pointer indicated by pointerId. The callback should return true if the user
* is permitted to drag the given view with the indicated pointer.
*
* <p>ViewDragHelper may call this method multiple times for the same view even if
* the view is already captured; this indicates that a new pointer is trying to take
* control of the view.</p>
*
* <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)}
* will follow if the capture is successful.</p>
*
* @param child Child the user is attempting to capture
* @param pointerId ID of the pointer attempting the capture
* @return true if capture should be allowed, false otherwise
*/
public abstract boolean tryCaptureView(@NonNull View child, int pointerId);
/**
* Restrict the motion of the dragged child view along the horizontal axis.
* The default implementation does not allow horizontal motion; the extending
* class must override this method and provide the desired clamping.
*
*
* @param child Child view being dragged
* @param left Attempted motion along the X axis
* @param dx Proposed change in position for left
* @return The new clamped position for left
*/
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return 0;
}
/**
* Restrict the motion of the dragged child view along the vertical axis.
* The default implementation does not allow vertical motion; the extending
* class must override this method and provide the desired clamping.
*
*
* @param child Child view being dragged
* @param top Attempted motion along the Y axis
* @param dy Proposed change in position for top
* @return The new clamped position for top
*/
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return 0;
}
}
2.ViewDragHelper.Callback的回調(diào)方法
接下來我們就重點來了解下我們需要用到的ViewDragHelper.Callback中的回調(diào)方法
首先我們先來介紹一下控制子控件產(chǎn)生拖動效果的回調(diào)方法tryCaptureView痕惋、clampViewPositionHorizontal和clampViewPositionVertical
tryCaptureView:是控制當(dāng)前觸摸的子控件是否可以被拖動
clampViewPositionHorizontal:控制子控件橫向拖動的位置(通過改變子控件的left值),這個方法返回的即是子控件left最終的值
clampViewPositionVertical:控制子控件縱向拖動的位置(通過改變子控件top值),這個方法返回的即是子控件top最終的值
我們看下我們的案例代碼中怎么使用的:
private ViewDragHelper mViewDragHelper;
private int mMaxExpandOffset = 1400;//最大展開距離
private void init() {
mViewDragHelper = ViewDragHelper.create(this, 1f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
//需要滑動的子控件就返回true(這里我們通過id來規(guī)定的)
return child.getId() == R.id.scroll_container;
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return 0;
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
if(top > mMaxExpandOffset){
//當(dāng)前滑動的控件是需要滑動的控件娃殖,如果向下滑動的距離超過了最大的展開距離那就返回設(shè)置的最大距離
return mMaxExpandOffset;
}else if(top > 0){
//當(dāng)前滑動的控件是需要滑動的控件值戳,如果向下滑動的距離沒超過最大的展開距離那就按手指的拖動進(jìn)行移動
return top;
}else {
//滑動距離小于0的時候就返回0(即不動)
return 0;
}
}
});
}
對照著以上代碼我們可以知道,我們是通過限定子控件的id來讓特定的子控件(id為R.id.scroll_container的子控件)可以拖動炉爆,由于我們只需要縱向拖動因此我們將橫向拖動的值始終返回0(即橫向永遠(yuǎn)不動)堕虹,然后我們在縱向拖動的回調(diào)方法中限定了滑動的范圍(這里我們暫時設(shè)置mMaxExpandOffset為1400)
3.onInterceptTouchEvent和onTouchEvent的處理
另外我們還需要在我們自定義布局的onInterceptTouchEvent和onTouchEvent中進(jìn)行相應(yīng)的處理(即需要把事件傳給ViewDragHelper處理)這是固定的,代碼如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}
進(jìn)行到這里實現(xiàn)的效果如下:
我們發(fā)現(xiàn)現(xiàn)在并不能做到滑動超過某個范圍后自動展開或恢復(fù)芬首,所以我們就需要用到ViewDragHelper.Callback中的另外一個方法onViewReleased來進(jìn)行處理了
4.實現(xiàn)手指抬起自動展開或收回
我們在ViewDragHelper.Callback的手指抬起的回調(diào)方法(onViewReleased)中做如下處理赴捞,即設(shè)置一個標(biāo)志(mIsExpand)用來記錄展開還是收起狀態(tài),然后我們根據(jù)手指抬起時的top值來判斷當(dāng)前控件的滑動距離如果超出了我們設(shè)置的標(biāo)準(zhǔn)(這里我們設(shè)置為mExpandOffset=300)那么就通過ViewDragHelper的smoothSlideViewTo方法讓控件自動展開或收起郁稍,而由于其內(nèi)部是使用Scroller實現(xiàn)的赦政,因此我們還需要在我們的自定義控件中重寫computeScroll方法,至于為什么需要重寫?感興趣的同學(xué)可以看下我的這篇文章:Android Scroller使用(附列表滑動刪除案例)
private ViewDragHelper mViewDragHelper;
private boolean mIsExpand = false;//是否展開
private int mMaxExpandOffset = 1400;//最大展開距離
private int mExpandOffset = 300;//可以觸發(fā)展開或收起所滑動的最小距離
private void init() {
mViewDragHelper = ViewDragHelper.create(this, 1f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
//需要滑動的子控件就返回true(這里我們通過id來規(guī)定的)
return child.getId() == R.id.scroll_container;
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return 0;
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
if(top > mMaxExpandOffset){
//當(dāng)前滑動的控件是需要滑動的控件恢着,如果向下滑動的距離超過了最大的展開距離那就返回設(shè)置的最大距離
return mMaxExpandOffset;
}else if(top > 0){
//當(dāng)前滑動的控件是需要滑動的控件桐愉,如果向下滑動的距離沒超過最大的展開距離那就按手指的拖動進(jìn)行移動
return top;
}else {
//滑動距離小于0的時候就返回0(即不動)
return 0;
}
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
if(mIsExpand){
if(mMaxExpandOffset - releasedChild.getTop() >= mExpandOffset){
//已經(jīng)展開設(shè)置關(guān)閉
mIsExpand = false;
mViewDragHelper.smoothSlideViewTo(releasedChild,0,0);
}else {
mViewDragHelper.smoothSlideViewTo(releasedChild,0,mMaxExpandOffset);
}
}else {
if(releasedChild.getTop() >= mExpandOffset){
//沒有展開,設(shè)置展開
mIsExpand = true;
mViewDragHelper.smoothSlideViewTo(releasedChild,0,mMaxExpandOffset);
}else {
mViewDragHelper.smoothSlideViewTo(releasedChild,0,0);
}
}
invalidate();
}
});
}
@Override
public void computeScroll() {
if(mViewDragHelper != null && mViewDragHelper.continueSettling(true)){
invalidate();
}
}
這樣的話就實現(xiàn)了基本的效果
效果優(yōu)化
我們發(fā)現(xiàn)雖然基本效果實現(xiàn)了掰派,但是還是存在某些問題的从诲,比如給我們的這個自定義控件的子View加一個點擊事件,那么在這個子View上進(jìn)行上下滑動的時候是劃不動的靡羡,這是因為子View在頂層消費(fèi)了觸摸事件所以ViewDragHelper不起作用了系洛,因此我們還需要處理ViewDragHelper.Callback中的getViewVerticalDragRange方法來開啟ViewDragHelper.shouldInterceptTouchEvent(event)縱向的狀態(tài)捕捉功能,如下:
@Override
public int getViewVerticalDragRange(@NonNull View child) {
//默認(rèn)為0略步,我們這里需要將它設(shè)置為1
return 1;
}
另外我們還發(fā)現(xiàn)碎罚,假如需要滑動的子控件為ScrollView的話ScrollView就滑不動了,這是因為父控件將ScrollView的滑動事件給攔截了纳像,我們需要做如下處理,即當(dāng)ScrollView沒有觸頂?shù)臅r候屏取消父控件的攔截:
private View mScrollView;//滑動的View
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//存儲滑動的子控件
if(mScrollView == null){
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
if(childAt.getId() == R.id.scroll_container){
mScrollView = childAt;
break;
}
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//子控件如果是ScrollView的話就判斷是否觸頂拯勉,如果不是在頂部就按默認(rèn)的方式處理竟趾,不讓ViewDragHelper處理
if(mScrollView != null && mScrollView instanceof ScrollView && mScrollView.getScrollY() != 0){
return super.onInterceptTouchEvent(ev);
}
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
我們還可以根據(jù)自己需要加一些其他控件的適配,或者加一個滑動值的回調(diào)宫峦,可以實現(xiàn)標(biāo)題欄透明度變化的效果岔帽,如下:
案例源碼
https://gitee.com/itfitness/scroll-layout
額外補(bǔ)充
所謂條條大路通羅馬,這里是我閑暇時用Scroller實現(xiàn)的一樣的效果的自定義View导绷,在這也分享下可供大家參考
案例源碼:https://gitee.com/itfitness/scroller-scroll-layout