Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 自定義View UC下拉刷新效果(二)

Android 自定義View UC下拉刷新效果(二)

編輯:關於Android編程

啦啦啦,這是山寨UC浏覽器的下拉刷新效果的第二篇,第一篇請移步Android 自定義View UC下拉刷新效果(一)
我們看圖說話:
HeaderRefreshLayout

主要工作

1.下拉刷新的圓形向回首頁的圓形的過度以及返回的效果。
2.View的事件分發等等。
3.相關接口回調。

對於第一塊,就是這個切換是的效果,其實在Android drawPath實現QQ拖拽泡泡我的第一篇文章中就講了,主要就是使用貝塞爾曲線來實現的。

只是這裡我試著使用了四階的貝塞爾曲線,因為控制點如果就一個的話,看起來有時候會覺得那個弧度拉得特別的尖,一點都不好看,而且我山寨的這個效果也沒有UC的那個那麼帥氣,可能還需要做相關的改進,如果你有好的點子請記得給我留言,一起完善嘛!!

private void drawSecPath(Canvas canvas) {
    path.reset();
    path.moveTo((float) (secondRectf.centerX() + Math.cos(180 / Math.PI * 30) * (secondRectf.centerX() - secondRectf.left)), (float) (secondRectf.centerY() + Math.sin(180 / Math.PI * 30) * (secondRectf.centerY() - secondRectf.top)));
    path.cubicTo(secondRectf.centerX() - 10*density, secondRectf.centerY() - backpaths, secondRectf.centerX() + 10*density, secondRectf.centerY() - backpaths, (float) (secondRectf.centerX() - Math.cos(180 / Math.PI * 30) * (secondRectf.centerX() - secondRectf.left)), (float) (secondRectf.centerY() + Math.sin(180 / Math.PI * 30) * (secondRectf.centerY() - secondRectf.top)));
    //path.quadTo(secondRectf.centerX(), secondRectf.centerY() - backpaths, (float) (secondRectf.centerX() - Math.cos(180 / Math.PI * 30) * (secondRectf.centerX() - secondRectf.left)), (float) (secondRectf.centerY() + Math.sin(180 / Math.PI * 30) * (secondRectf.centerY() - secondRectf.top)));
    canvas.drawArc(secondRectf, 0, 360, true, secPaint);
    canvas.drawPath(path, secPaint);
    //drawArc(canvas);
}

private void drawFirstPath(Canvas canvas) {
    path.reset();
    path.moveTo((float) (outRectF.centerX() - Math.cos(180 / Math.PI * 30) * (outRectF.centerX() - outRectF.left)), (float) (outRectF.centerY() - Math.sin(180 / Math.PI * 30) * (outRectF.centerY() - outRectF.top)));
        //path.quadTo(outRectF.centerX(), outRectF.centerY() + paths, (float) (outRectF.centerX() + Math.cos(180 / Math.PI * 30) * (outRectF.centerX() - outRectF.left)), (float) (outRectF.centerY() - Math.sin(180 / Math.PI * 30) * (outRectF.centerY() - outRectF.top)));
    path.cubicTo(outRectF.centerX() + 10 * density, outRectF.centerY() + paths, outRectF.centerX() - 10 * density, outRectF.centerY() + paths, (float) (outRectF.centerX() + Math.cos(180 / Math.PI * 30) * (outRectF.centerX() - outRectF.left)), (float) (outRectF.centerY() - Math.sin(180 / Math.PI * 30) * (outRectF.centerY() - outRectF.top)));
    canvas.drawArc(outRectF, 0, 360, true, paint);
    canvas.drawPath(path, paint);
    drawArc(canvas);
}

這裡兩個控制點的偏移量是寫死的,而不是根據圓形的size的百分比計算出來的,所以如果你修改了圓形的半徑,那麼這裡可能會出現小小的問題,需要手動完善下!

下拉刷新

其實現在的下拉刷新也是爛大街的,就我現在理解的下拉刷新其實有兩種模式了,一種是之前的寫好一個頭布局在那個HeaderLayout中,然後margin將其隱藏掉,然後在下拉的時候攔截相關事件,決定是否應該讓Header顯示出來。攔截的條件就是子View(ListView ScrollView RecycleView等等是否在頂部了而且手勢是向下拉(dy<0)!)

今天我們不說這種下拉,而是介紹Google在Android5.0(希望我沒有記錯 )提供的嵌套滑動的新機制

向下兼容的問題

從API21(就是5.0開始),ViewParent的接口裡面多了onStartNestedScroll()、onStopNestedScroll()等等的方法!當然,對應的ViewGroup中也有了這些方法,目測是空實現,因為它實現了這個接口嘛。那麼問題來了,如果你要向下兼容腫麼辦呢?!
這裡有supportV4包來提供向下兼容,不會寫不懂這玩意兒不著急,想想Android新的控件(RecycleView SwipeRefreshLayout NestedScrollView)這些都是支持嵌套滑動滴。。

相關接口方法

NestedScrollingParent和NestedScrollingChild這兩個接口就是用來實現相關的向下兼容的方法滴。。

This interface should be implemented by ViewGroup subclasses that wish to support scrolling operations delegated by a nested child view.
Classes implementing this interface should create a final instance of a NestedScrollingParentHelper as a field and delegate any View or ViewGroup methods to the NestedScrollingParentHelper methods of the same signature.
Views invoking nested scrolling functionality should always do so from the relevant ViewCompat, ViewGroupCompat or ViewParentCompat compatibility shim static methods. This ensures interoperability with nested scrolling views on Android 5.0 Lollipop and newer.

這個是NestedScrollingParent自己的一番解釋,可以明確知道,在5.0或者更新的,什麼ViewCompat等就提供了相關支持了(這個就是前面我說的那個嘛!),然後兼容的話,就要用這個,而且還要使用一個叫NestedScrollingParentHelper的輔助類來統一處理一些東西。
NestedScrollingParent

NestedScrollingChild

然後是不是感覺要哔了狗了,這麼多方法要實現?!其實我也是醉醉的,然後打算抄抄別人的就好了!<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> private final NestedScrollingParentHelper mNestedScrollingParentHelper; private final NestedScrollingChildHelper mNestedScrollingChildHelper; //初始化兩個helper mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); mNestedScrollingChildHelper = new NestedScrollingChildHelper(this); setNestedScrollingEnabled(true);

然後各種實現的方法中:

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    return isEnabled() && canChildScrollUp() && !mReturningToStart && !mRefreshing
            && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
    // Reset the counter of how much leftover scroll needs to be consumed.
    mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
    // Dispatch up to the nested parent
    startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
    mTotalUnconsumed = 0;
    mNestedScrollInProgress = true;
}

 @Override
public boolean hasNestedScrollingParent() {
    return mNestedScrollingChildHelper.hasNestedScrollingParent();
}

@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed, int[] offsetInWindow) {
    return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
            dxUnconsumed, dyUnconsumed, offsetInWindow);
}

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}

@Override
public boolean onNestedPreFling(View target, float velocityX,
        float velocityY) {
    return dispatchNestedPreFling(velocityX, velocityY);
}

@Override
public boolean onNestedFling(View target, float velocityX, float velocityY,
        boolean consumed) {
    return dispatchNestedFling(velocityX, velocityY, consumed);
}
.......

新的嵌套滑動的分發機制:

      子View                                    parent
startNestedScroll       --->    onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll --->     onNestedPreScroll
dispatchNestedScroll    --->     onNestedScroll
stopNestedScroll        --->     onStopNestedScroll

所以說並不是很復雜,其實就是在以前的事件分發的基礎上給父View提供了一個消費事件的機會,以前的話,誰接受了DOWN事件,那麼之後所有的事件都會交給它處理,直到它不處理的時候才會又依次返回給父View,或者直到新的DOWN事件開始分發。

嵌套滑動的意思就是在子View處理相關事件的時候,可以根據情況反饋給父View,然後根據父View處理的結果再進行下一步的處理!

RecycleView實現了NestedScrollingChild,在TouchEvet()中有以下邏輯:

 switch (action) {
    case MotionEvent.ACTION_DOWN: {
        .....
        startNestedScroll(nestedScrollAxis);
    } break;

    case MotionEvent.ACTION_MOVE: {
        if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
            dx -= mScrollConsumed[0];
            dy -= mScrollConsumed[1];
            vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
            // Updated the nested offsets
            mNestedOffsets[0] += mScrollOffset[0];
            mNestedOffsets[1] += mScrollOffset[1];
        }

       .....
    } break;

    case MotionEvent.ACTION_UP: {

        resetTouch();
    } break;

   ......
}

private void resetTouch() {
    if (mVelocityTracker != null) {
        mVelocityTracker.clear();
    }
    stopNestedScroll();
    releaseGlows();
}

根據上面的代碼可以看出onNestedPreScroll(),這個就是在子View還沒有滑動之前會先走的,如果父View有相關消費,那麼子View會計算出父View消費的偏移量,繼續消費剩余的偏移量。而在子View的消費的過程中,它會計算出過程中並沒有消費的偏移量。

 if (y != 0) {
            consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
            unconsumedY = y - consumedY;
        }

然後回調dispatchNestedScroll,父View就可以在onNestedScroll()中進行處理了!

最後在UP或者CANCLE事件中,子View會stopNestedScroll(),然後父View就走到了onStopNestedScroll()。整個嵌套滑動到此結束!

具體實現

1.下拉的時候展現頭布局
這裡其實就是走onNestedScroll(),因為這個時候子View已經在頂部了,向下拉的dy偏移量它肯定消費不了,所以在onNestedScroll()中unconsumedY就是父View需要消費的。
2.下拉的過程中又開始向上滑動
這裡就需要注意了,這個時候,父View和子View都可以響應和消費對應的事件的,因為他們現在都是可以向上滑動的,但是這裡必須要父View優先消費事件,所以這裡就要在onNestedPreScroll()中做相關的處理。

  @Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    // if we're in a drag gesture and the user reverses up the we should take those events
    if (!header.ismRunning() && dy > 0 && totalDrag > defaulTranslationY) {
        Log.e(TAG, "onNestedPreScroll:消費 " + dy);
        updateOffset(dy);
        consumed[1] = dy;//通知子View我已經消費的偏移量
    }
}


@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
                           int dxUnconsumed, int dyUnconsumed) {
    if (!header.ismRunning() && dyUnconsumed < 0) {
        Log.e(TAG, "onNestedScroll:未消費:: " + dyUnconsumed);
        updateOffset(dyUnconsumed);
    }

}

OK,到這裡,嵌套滑動就基本好了!接下來就是控制頭布局的展現了!這裡就是直接讓子View向下移動,頭布局自然就出現了!然後將相關偏移量傳到之前的TouchCircleView中,完成相關動畫!

private void updateOffset(int dyUnconsumed) {

    totalDrag -= dyUnconsumed * 0.5;
    Log.i(TAG, "updateOffset: " + totalDrag);
    if (totalDrag < 0) {
        totalDrag = 0;
    }
    if (totalDrag > header.getHeight() * 1.5) {
        totalDrag = header.getHeight() * 1.5f;
    }
    if (targetView != null) {
        targetView.setTranslationY(totalDrag);
    }
    if (!header.ismRunning()) {
        header.handleOffset((int) (totalDrag));
    }

}

相關方法及回調

//設置為刷新的loading狀態
public void setRefresh(boolean refresh) {
    if (mRefresh == refresh) {
        return;
    }
    mRefresh = refresh;
    header.setRefresh(mRefresh);
}
//刷新失敗狀態
public void setRefreshError() {
    header.setRefreshError();
}
//刷新成功狀態
public void setRefreshSuccess() {
    header.setRefreshSuccess();
}

mHeader.addLoadingListener(new TouchCircleView.OnLoadingListener() {
        @Override
        public void onProgressStateChange(int state, boolean hide) {
            //狀態改變
        }

        @Override
        public void onProgressLoading() {
          //正在loading 加載相關數據!
        }
    });

相關Demo請移步我的github。。。

— Edit By Joe —

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved