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

這是山寨UC瀏覽器的下拉刷新效果的的結(jié)尾篇了观话,到這里,基本是實現(xiàn)了UC瀏覽器首頁的效果了越平!還沒有看之前的小伙伴記得出門左轉(zhuǎn)先看看喲(Android 自定義View UC下拉刷新效果(一)频蛔、Android 自定義View UC下拉刷新效果(二))。期間也有不小的改動秦叛,主要集中在那個小圓球拖拽時的繪制方式上晦溪,可以看到,最后的圓球效果比之前的順暢漂亮了很多U醢稀三圆!

pull.png
back.png
loading.png
PullRefreshfinal.gif

經(jīng)過前面的兩篇文章,分別從小球動畫和下拉刷新兩個方面介紹了相關(guān)的內(nèi)容避咆,最后還剩首頁顯示過渡列表展示的內(nèi)容了舟肉!效果說明:

  • 1、向上滑動牌借,背景和tab有個漸變效果
  • 2度气、向下滑動,有一個放大和圓弧出現(xiàn)

功能拆分

  • 1膨报、展開關(guān)閉top默認(rèn)值
    因為這里有兩種狀態(tài)磷籍,一種是展開的,一種是首頁的關(guān)閉狀態(tài)现柠,展開的默認(rèn)top是TabLayout的對應(yīng)高度加上自身的top值院领,而關(guān)閉時,默認(rèn)top值是上面的CurveView的高度加上自身的top值够吩。
  • 2比然、實現(xiàn)拖拽滑動效果
    首先想到的就是ViewDragHelper,使用它來控制相關(guān)的拖拽周循。
  • 3强法、拖拽背景漸變效果
    這個就是設(shè)置拖拽過程中相關(guān)的回調(diào)万俗。另外就是在首頁的狀態(tài),ViewPager是沒法左右滑動的饮怯。
  • 4闰歪、繪制下拉的弧度
    這個就得使用到drawPath()繪制貝塞爾曲線了。

相關(guān)對象介紹

父布局是一個CurveLayout,里面包含三個對象:

  // child views & helpers
private View sheet;//target
private ViewDragHelper sheetDragHelper;
private ViewOffsetHelper sheetOffsetHelper;

sheet就是我們的拖拽目標(biāo)View蓖墅,ViewDragHelper拖拽輔助類库倘,寫好對應(yīng)的事件處理和Callback就可以實現(xiàn)拖拽功能了!這里不詳細(xì)介紹论矾。ViewOffsetHelper,對于它的介紹教翩,可以看看下面的截圖:

ViewOffsetHelper.png

因為我們這里只涉及上下的移動,所以介紹以下主要方法:

//構(gòu)造方法
public ViewOffsetHelper(View view) {
    mView = view;
}
//onlayoutChange時調(diào)用
public void onViewLayout() {
    // Grab the intended top & left
    mLayoutTop = mView.getTop();
    mLayoutLeft = mView.getLeft();

    // And offset it as needed
    updateOffsets();
}
//View位置改變時調(diào)用該方法
public boolean setTopAndBottomOffset(int absoluteOffset) {
    if (mOffsetTop != absoluteOffset) {
        mOffsetTop = absoluteOffset;
        updateOffsets();
        return true;
    }
    return false;
}
//同步
public void resyncOffsets() {
    mOffsetTop = mView.getTop() - mLayoutTop;
    mOffsetLeft = mView.getLeft() - mLayoutLeft;
}
//更新值
private void updateOffsets() {
    ViewCompat.offsetTopAndBottom(mView, mOffsetTop - (mView.getTop() - mLayoutTop));
    ViewCompat.offsetLeftAndRight(mView, mOffsetLeft - (mView.getLeft() - mLayoutLeft));
}

展開贪壳、關(guān)閉的默認(rèn)top值

@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
    if (sheet != null) {
        throw new UnsupportedOperationException("CurveLayout must only have 1 child view");
    }
    sheet = child;
    sheetOffsetHelper = new ViewOffsetHelper(sheet);
    sheet.addOnLayoutChangeListener(sheetLayout);
    // force the sheet contents to be gravity bottom. This ain't a top sheet.
    ((LayoutParams) params).gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
    super.addView(child, index, params);
}

在addView()的方法中我們確定對應(yīng)的Target饱亿,然后為其設(shè)置一個OnLayoutChangeListener

//設(shè)置默認(rèn)的dismissTop值
public void setDismissOffset(int dismissOffset) {
    this.dismissOffset = currentTop + dismissOffset;
}
//設(shè)置默認(rèn)的expandTop值
public void setExpandTopOffset(int tabOffset) {
    if (this.expandTopOffset != tabOffset) {
        this.expandTopOffset  = tabOffset;
        sheetExpandedTop = currentTop + expandTopOffset;
    }
}

接下來看看OnLayoutChangeListener里面的相關(guān)邏輯:

private final OnLayoutChangeListener sheetLayout = new OnLayoutChangeListener() {
    @Override
    public void onLayoutChange(View v, int left, int top, int right, int bottom,
                               int oldLeft, int oldTop, int oldRight, int oldBottom) {

        sheetExpandedTop = top + expandTopOffset;
        sheetBottom = bottom;
        currentTop = top;
        sheetOffsetHelper.onViewLayout();

        // modal bottom sheet content should not initially be taller than the 16:9 keyline
        if (!initialHeightChecked) {
            applySheetInitialHeightOffset(false, -1);
            initialHeightChecked = true;
        } else if (!hasInteractedWithSheet
                && (oldBottom - oldTop) != (bottom - top)) { /* sheet height changed */
            /* if the sheet content's height changes before the user has interacted with it
               then consider this still in the 'initial' state and apply the height constraint,
               but in this case, animate to it */
            applySheetInitialHeightOffset(true, oldTop - sheetExpandedTop);
        }
        Log.e(TAG, "onLayoutChange: 布局變化了A认路捧!" + sheet.getTop());
    }
}; 

初始化sheetExpandedTop,currentTop等字段,并且調(diào)用上面提到的onViewLayout()传黄,同步ViewOffsetHelper的值。

拖拽滑動實現(xiàn)

ViewDragHelper就不多說了队寇,Android自帶的輔助類膘掰,添加一個Callback,然后處理相關(guān)回調(diào)方法就可以了佳遣!
判斷是否攔截處理事件:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    currentX = ev.getRawX();
    Log.e(TAG, "BottomSheet onInterceptTouchEvent: " + currentX);
    if (isExpanded()) {
        sheetDragHelper.cancel();
        return false;
    }
    hasInteractedWithSheet = true;

    final int action = MotionEventCompat.getActionMasked(ev);
    if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
        sheetDragHelper.cancel();
        return false;
    }
    return isDraggableViewUnder((int) ev.getX(), (int) ev.getY())
            && (sheetDragHelper.shouldInterceptTouchEvent(ev));
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    currentX = ev.getRawX();
    sheetDragHelper.processTouchEvent(ev);
    return sheetDragHelper.getCapturedView() != null || super.onTouchEvent(ev);
}

這里獲取的這個currentX是為了在下拉出現(xiàn)那個弧度的頂點识埋。在接下來的回調(diào)中會使用。

private final ViewDragHelper.Callback dragHelperCallbacks = new ViewDragHelper.Callback() {

    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        return child == sheet && !isExpanded();//是否可以拖拽
    }

    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        //豎直方向的值
        return Math.min(Math.max(top, sheetExpandedTop), sheetBottom);
    }

    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        return sheet.getLeft();
    }

    @Override
    public int getViewVerticalDragRange(View child) {
        //豎直方向的拖拽范圍
        return sheetBottom - sheetExpandedTop;
    }

    @Override
    public void onViewPositionChanged(View child, int left, int top, int dx, int dy) {
        // view的拖拽過程中
        reverse = false;
        //change的過程中通知同步改變
        sheetOffsetHelper.resyncOffsets();
        dispatchPositionChangedCallback();
        canUp = Math.abs(top - dismissOffset) > MIN_DRAG_DISTANCE;
    }

    @Override
    public void onViewReleased(View releasedChild, float velocityX, float velocityY) {
        //松手后
        boolean expand = canUp || Math.abs(velocityY) > MIN_FLING_VELOCITY;
        reverse = false;
        animateSettle(expand ? sheetExpandedTop: dismissOffset, velocityY);
    }
};

可以看到零渐,在onViewPositionChanged()的方法中會去調(diào)用resyncOffsets()的方法同步ViewOffsetHelper的對應(yīng)值窒舟。
onViewReleased()的方法中調(diào)用了animateSettle()的方法,兩種情況诵盼,一種是展開惠豺,一種是關(guān)閉(首頁的狀態(tài)),所以這里有一個expand的變量來標(biāo)識风宁,如果展開洁墙,就展開到sheetExpandedTop的高度,關(guān)閉的話戒财,那么就是到dismissOffset的高度热监。

animateSettle()方法最終執(zhí)行以下方法邏輯:

private void animateSettle(int initialOffset, final int targetOffset, float initialVelocity) {
    if (settling) return;
    Log.e(TAG, "animateSettle:TopAndBottom :::" + sheetOffsetHelper.getTopAndBottomOffset());
    if (sheetOffsetHelper.getTopAndBottomOffset() == targetOffset) {
        if (targetOffset >= dismissOffset) {
            dispatchDismissCallback();
        }
        return;
    }

    settling = true;
    final boolean dismissing = targetOffset == dismissOffset;
    final long duration = computeSettleDuration(initialVelocity, dismissing);
    final ObjectAnimator settleAnim = ObjectAnimator.ofInt(sheetOffsetHelper,
            ViewOffsetHelper.OFFSET_Y,
            initialOffset,
            targetOffset);
    settleAnim.setDuration(duration);
    settleAnim.setInterpolator(getSettleInterpolator(dismissing, initialVelocity));
    settleAnim.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            dispatchPositionChangedCallback();
            if (dismissing) {
                dispatchDismissCallback();
            }
            settling = false;
        }
    });
    if (callbacks != null && !callbacks.isEmpty()) {
        settleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if (animation.getAnimatedFraction() > 0f) {
                    dispatchPositionChangedCallback();
                }
            }
        });
    }
    settleAnim.start();
}

這里有一個settleAnim的屬性動畫,傳入的是ViewOffsetHelper里面的OFFSET_Y,在OFFSET_Yset()方法中饮寞,調(diào)用setTopAndBottomOffset()的方法去修改對應(yīng)的top值孝扛,從而實現(xiàn)了松手后展開或者關(guān)閉的動畫效果列吼。

final ObjectAnimator settleAnim = ObjectAnimator.ofInt(sheetOffsetHelper,
        ViewOffsetHelper.OFFSET_Y,
        initialOffset,
        targetOffset);


public static final Property<ViewOffsetHelper, Integer> OFFSET_Y =
      AnimUtils.createIntProperty(
              new AnimUtils.IntProp<ViewOffsetHelper>("topAndBottomOffset") {
          @Override
          public void set(ViewOffsetHelper viewOffsetHelper, int offset) {
              viewOffsetHelper.setTopAndBottomOffset(offset);
          }

          @Override
          public int get(ViewOffsetHelper viewOffsetHelper) {
              return viewOffsetHelper.getTopAndBottomOffset();
          }
      });

拖拽背景漸變效果

說到背景的漸變效果,那么肯定就是要講相關(guān)的回調(diào)了苦始!Callbacks用來處理對應(yīng)的回調(diào)冈欢,提供了三個方法:onSheetNarrowed(),onSheetExpanded(),onSheetPositionChanged(),分別對應(yīng)的時候關(guān)閉了,展開了盈简,和改變了三種情況凑耻。

onSheetPositionChanged(int sheetTop, float currentX, int dy, boolean userInteracted)的方法中,有四個參數(shù)柠贤,分別是當(dāng)前的top值香浩,當(dāng)前touch的x值,豎直方向的改變值臼勉,以及是否是由開到關(guān)或者由關(guān)到開的情況邻吭。

public static abstract class Callbacks {

    public void onSheetNarrowed() {

    }

    public void onSheetExpanded() {
    }

    public void onSheetPositionChanged(int sheetTop, float currentX, int dy, boolean userInteracted) {
    }
}

public void registerCallback(Callbacks callback) {
    if (callbacks == null) {
        callbacks = new CopyOnWriteArrayList<>();
    }
    callbacks.add(callback);
}

public void unregisterCallback(Callbacks callback) {
    if (callbacks != null && !callbacks.isEmpty()) {
        callbacks.remove(callback);
    }
}

在具體是實現(xiàn)中是這樣的:

    mBoottom.registerCallback(new CurveLayout.Callbacks() {
        private int dy;

        @Override
        public void onSheetExpanded() {
            Log.e(TAG, "onSheetExpanded: ");
            mCurveView.onDispatchUp();
            mCurveView.setTranslationY(0);
            mCurveView.setVisibility(View.GONE);
            mTab.setTranslationY(-mCurveView.getHeight());
            mTab.setVisibility(View.VISIBLE);
            mCurveView.setScaleX(1.f);
            mCurveView.setScaleY(1.f);
            mViewPager.setScrollable(true);
            dy = 0;
        }

        @Override
        public void onSheetNarrowed() {
            Log.e(TAG, "onSheetNarrowed: ");
            mCurveView.onDispatchUp();
            mCurveView.setTranslationY(0);
            mCurveView.setScaleX(1.f);
            mCurveView.setScaleY(1.f);
            mTab.setVisibility(View.GONE);
            mViewPager.setScrollable(false);
            mCurveView.setVisibility(View.VISIBLE);
            dy = 0;

        }

        @Override
        public void onSheetPositionChanged(int sheetTop, float currentX, int ddy, boolean reverse) {

            if (mCurveViewHeight == 0) {
                mCurveViewHeight = mCurveView.getHeight();
                mBoottom.setDismissOffset(mCurveViewHeight);
            }
            this.dy += ddy;
            float fraction = 1 - sheetTop * 1.0f / mCurveViewHeight;
            if (!reverse) {
                if (fraction >= 0 && !mBoottom.isExpanded()) {//向上拉
                    mTab.setVisibility(View.VISIBLE);
                    mBoottom.setExpandTopOffset(mTab.getHeight());
                    mCurveView.setTranslationY(dy * 0.2f);
                    mTab.setTranslationY(-fraction * (mCurveView.getHeight() + mTab.getHeight()));
                } else if (fraction < 0 && !mBoottom.isExpanded()) {//向下拉
                    mTab.setVisibility(View.GONE);
                    mCurveView.onDispatch(currentX, dy);
                    mCurveView.setScaleX(1 - fraction * 0.5f);
                    mCurveView.setScaleY(1 - fraction * 0.5f);
                }
            }
        }
    });

可以看到,在onSheetPositionChanged()的方法中宴霸,首先是進(jìn)行了一些值的初始化囱晴,然后根據(jù)reverse來判斷,如果不是由開到關(guān)或者由關(guān)到開的狀態(tài)改變瓢谢,那么就開始背景的移動或者背景的放大及畫出對應(yīng)的弧形畸写。另外在onSheetNarrowed()或者onSheetExpanded()中就是對View做的一些初始化或者重置操作!

繪制下拉的弧度

當(dāng)是下拉的時候氓扛,需要繪制出弧形枯芬,這里使用到了CurveView以及它的onDispatch()方法!

@Override
protected void onDraw(Canvas canvas) {
    path.reset();
    path.moveTo(0, getMeasuredHeight());
    path.quadTo(currentX, currentY + getMeasuredHeight(), getWidth(), getMeasuredHeight());
    canvas.drawPath(path, paint);
}

public void onDispatch(float dx, float dy) {
    currentY = dy > MAX_DRAG ? MAX_DRAG : dy;
    currentX = dx;
    if (dy > 0) {
        invalidate();
    }
}

其實很簡單采郎,就是使用當(dāng)前的X值的坐標(biāo)和dy的值來進(jìn)行drawPath()的操作千所。當(dāng)然這里有一個上限的限制。

到這里蒜埋,實現(xiàn)拖拽展開及關(guān)閉的邏輯就實現(xiàn)完成了淫痰,總結(jié)起來就是使用ViewDragHelper來輔助實現(xiàn)拖拽功能,在松手的時候調(diào)用ViewOffsetHelper來實現(xiàn)展開或者關(guān)閉的漸變動畫效果整份,期間調(diào)用Callbacks回調(diào)對應(yīng)的狀態(tài)(展開了待错、關(guān)閉了、位置變化了)皂林。

圓球繪制邏輯改動

之前的第一篇文章中介紹的圓球拉伸繪制時采用的是drawArc()和drawPath結(jié)合的方法朗鸠,所以看著總覺得有點兒怪,然后查了相關(guān)的資料础倍,這里使用了新的方式烛占,請看圖:


網(wǎng)上拷的

意思就是一個圓形,可以理解為是采用了drawPath()畫了四段弧。每段弧就是使用path.cubicTo()繪制的貝塞爾曲線忆家。

根據(jù)網(wǎng)上的資料犹菇,這里的m的值就是半徑R*0.551915024494f。在豎直方向拖拽的過程中芽卿,其實就是改變這12個點的坐標(biāo)揭芍,從而繪制出想要的弧形。

項目下載:https://github.com/lovejjfg/UCPullRefresh

喜歡就請點個Start唄卸例。称杨。

參考資料

1、Plaid項目
2筷转、三次貝塞爾曲線練習(xí)之彈性的圓

---- Edit By Joe ----

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末姑原,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子呜舒,更是在濱河造成了極大的恐慌锭汛,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件袭蝗,死亡現(xiàn)場離奇詭異唤殴,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)到腥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門朵逝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人左电,你說我怎么就攤上這事廉侧。” “怎么了篓足?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長闰蚕。 經(jīng)常有香客問我栈拖,道長,這世上最難降的妖魔是什么没陡? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任涩哟,我火速辦了婚禮,結(jié)果婚禮上盼玄,老公的妹妹穿的比我還像新娘贴彼。我一直安慰自己,他們只是感情好埃儿,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布器仗。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪精钮。 梳的紋絲不亂的頭發(fā)上威鹿,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機(jī)與錄音轨香,去河邊找鬼忽你。 笑死,一個胖子當(dāng)著我的面吹牛臂容,可吹牛的內(nèi)容都是我干的科雳。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼脓杉,長吁一口氣:“原來是場噩夢啊……” “哼糟秘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起丽已,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蚌堵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后沛婴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吼畏,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年嘁灯,在試婚紗的時候發(fā)現(xiàn)自己被綠了泻蚊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡丑婿,死狀恐怖性雄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情羹奉,我是刑警寧澤秒旋,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站诀拭,受9級特大地震影響迁筛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜耕挨,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一细卧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧筒占,春花似錦贪庙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春农尖,著一層夾襖步出監(jiān)牢的瞬間析恋,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工盛卡, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留助隧,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓滑沧,卻偏偏與公主長得像并村,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子滓技,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容