亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android添加自定義下拉刷新布局阻尼滑動懸停彈動畫效果

 更新時間:2023年02月17日 08:47:47   作者:李暖光  
這篇文章主要為大家介紹了Android添加自定義下拉刷新布局阻尼滑動懸停彈動畫效果詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

Android 對現(xiàn)有布局添加下拉刷新

先直接上效果,如下GIF所示

一、簡述

對現(xiàn)有布局添加一個下拉刷新,并且這個動畫的效果如上GIF所示

1、下拉階段

下拉過程中,有阻尼滑動效果

2、下拉松手階段

(1)、進行高度判斷,若大于指定的高度后,先回彈到指定的高度后,做懸停動畫效果,再然后做回彈動畫回彈到原始位置

(2)、若沒有大于指定的高度,則直接回彈到原始位置

(3)刷新的時機,可以自由選擇,例如在松手時,即發(fā)起刷新邏輯。

二、現(xiàn)有布局

如前面的GIF所示,藍色區(qū)域是內(nèi)容區(qū)域,即是添加下拉刷新前的現(xiàn)有布局

三、添加下拉刷新

從GIF圖可以看出,添加下拉刷新,需要兩個控件:一個響應下拉操作的父容器控件、一個是刷新頭部控件

下拉刷新的主要思路:

頁面布局:將響應下拉操作的父容器控件包裹紅色下拉刷新頭部區(qū)域 和 藍色內(nèi)容區(qū)域,其中藍色內(nèi)容區(qū)域覆蓋在紅色下拉刷新頭部區(qū)域的上面。

下拉操作:下拉時,動態(tài)地改變紅色下拉刷新頭部區(qū)域的高度,以及動態(tài)改變藍色內(nèi)容區(qū)域的marginTop值

然后,就是動畫操作,也是動態(tài)地改變紅色下拉刷新頭部區(qū)域的高度 和 藍色內(nèi)容區(qū)域的marginTop值。

1、一個響應下拉操作的父容器控件

為寫起來簡單,直接繼承RelativeLayout,重點重寫onInterceptTouchEvent 和 onTouchEvent方法。

(1)onInterceptTouchEvent

攔截事件方法:

首先,判斷該事件是否需要攔截;

然后,若攔截該事件:在down事件時,將之前操作紅色下拉刷新頭部區(qū)域 及 藍色內(nèi)容區(qū)域都重置下

然后,在move事件時,判斷當前移動的距離是否 > mTouchSlop(表示滑動的最小距離) ,當大于時,認為此時產(chǎn)生了拖拽滑動

最后,在up\cancel事件時,將拖拽標志 重置回來

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    if (不攔截事件的判斷條件) {
        return false;
    }
    if (若此時正在執(zhí)行動畫,則攔截該事件) {
        return true;
    }
    final int action = event.getActionMasked();//獲取觸控手勢
    switch (action) {
    case MotionEvent.ACTION_DOWN:
        // 重置操作
        updateHeightAndMargin(0);
        mIsDragging = false;
        // 手指按下的距離
        this.mDownY = event.getY();
        break;
    case MotionEvent.ACTION_MOVE:
        final float y = event.getY();
        final float yDiff = y - this.mDownY;
        if (yDiff > mTouchSlop) {
            //判斷是否時產(chǎn)生了拖拽
            mIsDragging = true;
        }
        break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL:
        mIsDragging = false;
        break;
    default:
        break;
    }
    return mIsDragging;
}

(2)onTouchEvent

觸摸事件處理方法:

若此時沒有發(fā)生拖拽,或者此時正在動畫中: 不處理該事件

當在move事件時:計算阻尼滑動距離,然后更新給紅色的下拉刷新頭部區(qū)域 及 藍色的內(nèi)容區(qū)域

當在up/cancel事件時: 開啟動畫邏輯

@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
    if (!mIsDragging || mIsAnimation) {
        return super.onTouchEvent(event);
    }
    //獲取觸控手勢
    final int action = event.getActionMasked();
    switch (action) {
    case MotionEvent.ACTION_MOVE: {
        //獲取移動距離
        float eventY = event.getY();
        float yDiff = eventY - mDownY;
        float scrollTop = yDiff * 0.5;
        //計算實際需要被拖拽產(chǎn)生的移動百分比
        mDragPercent = scrollTop / mDp330;
        if (mDragPercent < 0) {
            return false;
        }
        //計算阻尼滑動的距離
        int targetY = (int) (computeTargetY(scrollTop, mDragPercent, mDp330) + 0.5f);
        updateHeightAndMargin(targetY);
        break;
    }
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL: {
        final float upDiffY = event.getY() - mDownY;
        final float overScrollTop = upDiffY * DEFAULT_DRAG_RATE;
        mIsDragging = false;
        if (overScrollTop > mDp54) {
            animateToHover();
        } else {
            animateToPeak();
        }
        mExtraDrag = 0;
        mPullRefreshBehavior.onUp();
        return false;
    }
    default:
        break;
    }
    return true;
}

阻尼滑動的計算方式:

/*計算阻尼滑動距離*/
public int computeTargetY(float scrollTop, float dragPercent, float maxDragDistance) {
    float boundedDragPercent = Math.min(1.0f, Math.abs(dragPercent));
    float extraOS = Math.abs(scrollTop) - maxDragDistance;
    float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, maxDragDistance * 2) / maxDragDistance);
    float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f;
    float extraMove = (maxDragDistance) * tensionPercent / 2;
    return (int) ((maxDragDistance * boundedDragPercent) + extraMove);
}

更新紅色頭部區(qū)域(mPullRefreshHeadView)高度 及 藍色的內(nèi)容區(qū)域(mTarget)

private void updateHeightAndMargin(int offsetTop) {
    if (mPullRefreshHeadView == null || mTarget == null) {
        return;
    }
    // 更新下拉刷新的頭部高度
    ViewGroup.LayoutParams headViewLayoutParams = mPullRefreshHeadView.getLayoutParams();
    if (headViewLayoutParams != null) {
        headViewLayoutParams.height = Math.max(offsetTop, mDp54);
    }
    // 更新 mTarget view 的 topMargin
    MarginLayoutParams targetLayoutParams = (MarginLayoutParams) mTarget.getLayoutParams();
    if (targetLayoutParams != null) {
        targetLayoutParams.topMargin = offsetTop;
    }
    mOffsetTop = offsetTop;
    mPullRefreshBehavior.onMove(mOffsetTop);
    // 刷新界面
    requestLayout();
}

2、下拉刷新頭部區(qū)域

這里可以根據(jù)自己的需求去構建下拉刷新頭部區(qū)域的布局,例如添加Lottie動畫等

代碼示例,是比較簡單的一個 Textview + 背景展示下

public class PullRefreshHeadView extends RelativeLayout {
    private View mHeaderView;
    public PullRefreshHeadView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }
    private void init(Context context) {
        Resources resources = context.getResources();
        mHeaderView =  LayoutInflater.from(context).inflate(R.layout.vivoshop_classify_pull_refresh_head, this, false);
        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, context.getResources().getDimensionPixelSize(R.dimen.dp54));
        params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
        params.addRule(RelativeLayout.CENTER_HORIZONTAL);
        params.bottomMargin = resources.getDimensionPixelSize(R.dimen.dp9);
        addView(mHeaderView, params);
    }
}

3、將下拉刷新頭部 及 內(nèi)容區(qū)域 引入到 響應下拉操作的父容器控件中

布局:響應下拉操作的父容器控件包裹著下拉刷新頭部及內(nèi)容區(qū)域

<?xml version="1.0" encoding="utf-8"?>
<com.qlli.pulllayout.PullRefreshLayout
    android:id="@+id/pull_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:background="@color/teal_700">
      <com.qlli.pulllayout.PullRefreshHeadView
          android:id="@+id/pull_header"
          android:layout_width="match_parent"
          android:layout_height="@dimen/dp54"
          android:background="@color/red"/>
      <RelativeLayout
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:background="@color/color_415fff"
          android:gravity="center"
          android:clickable="true">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/white"
                android:textSize="20sp"
                android:text="這里是內(nèi)容區(qū)域, 下拉試試看"/>
      </RelativeLayout>
</com.qlli.pulllayout.PullRefreshLayout>

在響應下拉操作的父容器控件初始化時,在onFinishInflate中將下拉刷新頭部、內(nèi)容區(qū)域分別進行賦值

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    ensureTargetView();
}
//尋找需要控制滑動的內(nèi)容區(qū)域的父容器
private void ensureTargetView() {
    if (mTarget != null || getChildCount() <= 0) {
        return;
    }
    for (int index = 0; index < getChildCount(); index++) {
        View child = getChildAt(index);
        if (child instanceof PullRefreshHeadView) {
            mPullRefreshHeadView = (PullRefreshHeadView) child;
            continue;
        }
        if (child != mPullRefreshHeadView) {
            mTarget = child;
            break;
        }
    }
}

4、回彈懸停動畫

回彈懸停動畫是指:先回彈到指定位置,然后開始懸停一段時間后,再開啟一個新的動畫

回彈動作:是指將 下拉刷新頭部 及 內(nèi)容區(qū)域 回彈至指定位置,可以在一個時間段中,通過監(jiān)聽0到100變化的,進而動態(tài)計算改變下拉刷新頭部及內(nèi)容區(qū)域的高度并更新

懸停動作:在回彈結(jié)束后,其實此時懸停是指回彈動畫結(jié)束后,就保持當前位置不動了,此時使用Handler發(fā)一個延時任務去執(zhí)行 一個新的回彈動畫(將下拉刷新及內(nèi)容區(qū)域回彈至原始位置),這個中間的過程給出的視覺效果是一個懸停的效果

private ValueAnimator mHoverAnimator;//回彈懸停動畫
private final Handler mHoverHandler = new Handler(Looper.getMainLooper());
private void animateToHover() {
    // 這里是內(nèi)容區(qū)域marginTop的距離
    final int startPosition = mOffsetTop;
    // 這里是動畫結(jié)束的位置,要保留一個下拉刷新頭部高度距離
    final int totalDistance = startPosition - mDp54;
    // 設置懸停動畫的一些初始化東西
    if (mHoverAnimator == null) {
        mHoverAnimator = ValueAnimator.ofFloat(0f, 100f);
        mHoverAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
    } else {
        mHoverAnimator.removeAllUpdateListeners();
        mHoverAnimator.removeAllListeners();
        mHoverAnimator.end();
    }
    // 在動畫監(jiān)聽過程中,通過updateHeightAndMargin移動下拉刷新及內(nèi)容區(qū)域的距離
    mHoverAnimator.addUpdateListener(animation -> {
        Object value = animation.getAnimatedValue();
        if (value instanceof Float) {
            float percent = ((float) value) / 100f;
            int targetTop = startPosition - (int) (totalDistance * percent);
            updateHeightAndMargin(targetTop);
        }
    });
    // 監(jiān)聽此動畫開始 和 結(jié)束點
    mHoverAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationStart(Animator animation) {
            mIsAnimation = true;
        }
        // 在該動畫結(jié)束后,在1.6s后,做一個回彈動畫,因此在1.6s的時間內(nèi)就是一個懸停效果
        // 可以在這個懸停的期間干些事情,例如播放Lottie動畫等
        @Override
        public void onAnimationEnd(Animator animation) {
            mHoverHandler.removeCallbacksAndMessages(null);
            mHoverHandler.postDelayed(() -> {
                if (isAttachedToWindow()) {
                    // 例如在這個播放Lottie動畫
                    ensureTargetView();
                    // 回彈動畫
                    animateToPeak();
                }
            }, 1600);
        }
    });
    // 此動畫設置一下時間
    float animationPercent = Math.min(1.0f, Math.abs(totalDistance) * 1.0f / mDp54);
    long duration = Math.abs((long) (ANIMATION_DURATION_300 * animationPercent));
    mHoverAnimator.setDuration(duration);
    mHoverAnimator.start();
}

5、回彈到頂部的動畫

這個回彈到頂部的操作是指:將下拉刷新頭部 及 內(nèi)容區(qū)域 在一定時間內(nèi) 回到頂部

private ValueAnimator mPeakAnimator;//回彈動畫
private void animateToPeak() {
    float startDragPercent = mDragPercent;
    //松手后開始從此位置滑動
    final int totalDistance = mOffsetTop;
    if (mPeakAnimator == null) {
        mPeakAnimator = ValueAnimator.ofFloat(0f, 100f);
        mPeakAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
    } else {
        mPeakAnimator.removeAllListeners();
        mPeakAnimator.removeAllUpdateListeners();
        mPeakAnimator.end();
    }
    mPeakAnimator.addUpdateListener(animation -> {
        Object value = animation.getAnimatedValue();
        if (value instanceof Float) {
            float percent = ((float) value) / 100f;
            int targetTop = (int) (totalDistance * (1.0f - percent));
            updateHeightAndMargin(targetTop);
        }
    });
    mPeakAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationStart(Animator animation) {
            mIsAnimation = true;
        }
        @Override
        public void onAnimationEnd(Animator animation) {
            mIsAnimation = false;
            updateHeightAndMargin(0);
        }
    });
    float ratio = Math.abs(startDragPercent);
    // 滑動到頂部的時間
    mPeakAnimator.setDuration((long) (800 * ratio));
    mPeakAnimator.start();
}

6、在某些時機下,進行回調(diào)

可以結(jié)合自己的需求寫一個接口,例如下面這樣:

public interface PullRefreshBehavior {
    // 移動的高度
    void onMove(int height);
    // 手指抬起
    void onUp();
    // 懸停
    void onHover();
    // 回彈
    void onSpringBack();
    // 完成
    void onComplete();
}

然后在下拉操作的過程中 去選擇性地調(diào)用 上面接口中的方法,這樣在實現(xiàn)該接口的具體實現(xiàn)類中,就能根據(jù)當前下拉操作的不同時機來去做一些想做的事情

四、遇到的問題

  • 1、在下拉操作時,在onInterceptTouchEvent方法時僅響應down事件,move事件不響應

導致該問題的主要原因是:響應下拉操作的父容器內(nèi)包裹的子控件沒有消耗down事件,所以后續(xù)收不到move事件

  • 2、看下ViewGroup中的事件分發(fā)這段代碼

可以看到下面代碼中: 是down事件,或者 mFirstTouchTarget != null

若父容器包裹的子控件沒有消耗down事件,則mFirstTouchTarget == null,那么當move事件到來是,即不滿足條件,則不會調(diào)用到 onInterceptTouchEvent方法。

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        intercepted = false;
    }
} else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}

如何解決呢

在子控件中,加一個消耗down事件的操作即可,例如在子控件布局中,添加一個clickable屬性為 true 即可

因為可點擊事件,是消耗down事件的

 <RelativeLayout
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:background="@color/color_415fff"
          android:gravity="center"
          android:clickable="true">

以上就是Android添加自定義下拉刷新布局阻尼滑動懸停彈動畫效果的詳細內(nèi)容,更多關于Android添加下拉刷新布局的資料請關注腳本之家其它相關文章!

相關文章

最新評論