1. 程式人生 > >SwipeRefreshLayout原始碼分析+自定義UC頭條下拉重新整理Demo

SwipeRefreshLayout原始碼分析+自定義UC頭條下拉重新整理Demo

首先來看SwipeRefreshLayout(以下簡稱SR)的繼承關係

image

NestedScrollingParent:巢狀滑動父介面

NestedScrollingChild :巢狀滑動子介面

Android 就是通過這兩個介面, 來實現 子View 與父View 之間的巢狀滑動

  • NestedScrollingChild:原始碼
public interface NestedScrollingChild {
    /**
     * Enable or disable nested scrolling for this view
     * 為這個檢視啟用或禁用巢狀滾動
     */
public void setNestedScrollingEnabled(boolean enabled); /** * Returns true if nested scrolling is enabled for this view. * 若啟動巢狀滑動,則返回True */ public boolean isNestedScrollingEnabled(); /** * Begin a nestable scroll operation along the given axes. * 在給定的軸上開始一個新的滾動操作。 * ViewCompat.SCROLL_AXIS_HORIZONTAL 橫向 * ViewCompat.SCROLL_AXIS_VERTICAL 縱向 */
public boolean startNestedScroll(int axes); /** * Stop a nested scroll in progress. * 停止巢狀的滾動 */ public void stopNestedScroll(); /** * Returns true if this view has a nested scrolling parent. * 如果該檢視有一個巢狀滾動的父檢視,則返回true。 */ public boolean hasNestedScrollingParent
(); /** * Dispatch one step of a nested scroll in progress. * * 在處理滑動之後 呼叫 * @param dxConsumed x軸上 被消費的距離 * @param dyConsumed y軸上 被消費的距離 * @param dxUnconsumed x軸上 未被消費的距離 * @param dyUnconsumed y軸上 未被消費的距離 * @param offsetInWindow view 的移動距離 * 如果事件被髮送,則返回true,如果該事件不能被髮送,則為false */ public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow); /** * Dispatch one step of a nested scroll in progress before this view consumes any portion of it. * *一般在滑動之前呼叫, 在ontouch 中計算出滑動距離, 然後呼叫該方法, 就給支援的巢狀的父View 處理滑動事件 * @param dx x 軸上滑動的距離, 相對於上一次事件, 不是相對於 down事件的 那個距離 * @param dy y 軸上滑動的距離 * @param consumed 一個數組, 可以傳 一個空的 陣列, 表示 x 方向 或 y 方向的事件 是否有被消費 * @param offsetInWindow 支援巢狀滑動到額父View 消費 滑動事件後 導致 本 View 的移動距離 * @return 支援的巢狀的父View 是否處理了 滑動事件 */ public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); /** * Dispatch a fling to a nested scrolling parent. * @param velocityX x 軸上的滑動速度 * @param velocityY y 軸上的滑動速度 * @param consumed 是否被消費 * @return */ public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); /** * Dispatch a fling to a nested scrolling parent before it is processed by this view. * *@param velocityX x 軸上的滑動速度 * @param velocityY y 軸上的滑動速度 * @return * @param velocityX Horizontal fling velocity in pixels per second * @param velocityY Vertical fling velocity in pixels per second * @return true if a nested scrolling parent consumed the fling */ public boolean dispatchNestedPreFling(float velocityX, float velocityY); }
  • NestedScrollingParent原始碼:

public interface NestedScrollingParent {
    /**
     * React to a descendant view initiating a nestable scroll operation, claiming thenested scroll operation if appropriate.
     * 對巢狀滾動的子View進行響應
     * 
     *
     * @param child ViewParent包含觸發巢狀滾動的view的物件
     * @param target觸發巢狀滾動的view(在這裡如果不涉及多層巢狀的話,child和ta   rget)是相同的
     * @param nestedScrollAxes 方向  ViewCompat.SCROLL_AXIS_HORIZONTAL
     *                         ViewCompat.SCROLL_AXIS_VERTICAL
     * @return true 如果ViewParent接受巢狀滾動操作,則返回true
     */
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    /**
     * React to the successful claiming of a nested scroll operation.
     * 對成功的使用巢狀滾動操作作出反應
     * @param child ViewParent包含觸發巢狀滾動的view的物件
     * @param target觸發巢狀滾動的view(在這裡如果不涉及多層巢狀的話,child和ta   rget)是相同的
     * @param nestedScrollAxes 滑動的方向          ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *  ViewCompat#SCROLL_AXIS_VERTICAL
     */
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    /**
     * React to a nested scroll operation ending.
     * 對一個巢狀滾動操作的結果進行響應
     * @param target 啟動滾動的View
     */
    public void onStopNestedScroll(View target);

    /**
     * React to a nested scroll in progress.
     * 對正在進行的巢狀滾動進行響應
     * @param target 控制滾動的子View
     * @param dxConsumed x軸消費的距離
     * @param dyConsumed y軸消費的距離
     * @param dxUnconsumed x軸未消費的距離
     * @param dyUnconsumed y軸未消費的距離
     */
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    /**
     * React to a nested scroll in progress before the target view consumes a portion of the scroll.
     *
     * @param target 控制滾動的子View
     * @param dx x軸消費總距離
     * @param dy y軸消費總距離
     * @param consumed Output. 父佈局分別在x,y軸消費的總距離:consumed[0],
     consumed[1]
     */
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    /**
     * Request a fling from a nested scroll.
     * 巢狀滑動的速度
     * @param target 控制滾動的子View
     * @param velocityX velocityX x 軸上的滑動速度 
     * @param velocityY y 軸上的滑動速度
     * @param consumed 子view是否消費
     * @return true if this parent consumed or otherwise reacted to the fling
     */
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    /**
     * React to a nested fling before the target view consumes it.
     *
     * @param target 控制滾動的子View
     * @param velocityX x 軸上的滑動速度 
     * @param velocityY y 軸上的滑動速度
     * @return 如果父佈局在這之前消費了該事件則返回True
     */
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    /**
     * Return the current axes of nested scrolling for this NestedScrollingParent.返回一個當前滑動軸,以下3種情況
     * @return Flags indicating the current axes of nested scrolling
     * @see ViewCompat#SCROLL_AXIS_HORIZONTAL
     * @see ViewCompat#SCROLL_AXIS_VERTICAL
     * @see ViewCompat#SCROLL_AXIS_NONE
     */
    public int getNestedScrollAxes();
}

這兩個介面的作用在上面的註釋中有詳細的解釋,下面就是最關鍵的SR原始碼的分析;因為SR繼承的是ViewGroup,我們平常都會自定義View,而自定義View通常都少不了:onMeasure(測量),onDraw(繪畫);而自定義ViewGroup會涉及到對子View的排版問題,所以在自定義ViewGroup中多了一個onLayout()方法需要我們處理,這些基本的問題解決後,若自定義控制元件涉及到觸控事件,也會需要我們對觸控事件的分發機制有一定的瞭解;然後就讓我們根據SR原始碼來一步一步分析下拉重新整理控制元件是怎樣實現的!(對原始碼的分析都是以程式碼的註釋的形式來進行的)

  • SR構造:
public SwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        //ViewConfiguration定義UI中用於超時、大小和距離的標準常量和獲取他們的值的方法
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
      //獲取動畫時間
        mMediumAnimationDuration = getResources().getInteger(
                android.R.integer.config_mediumAnimTime);
       //若沒有任何繪圖,則設定此方法(沒有重寫onDrow方法)
        setWillNotDraw(false);
        //設定減速插值器
        mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
        // 描述一個顯示的一般資訊的結構,例如它的大小、密度和字型大小。
        final DisplayMetrics metrics = getResources().getDisplayMetrics();
        mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);
        //建立頭部重新整理控制元件
        createProgressView();
        //告訴ViewGroup是否按照該方法定義的順序繪製它的孩子
        ViewCompat.setChildrenDrawingOrderEnabled(this, true);
        // the absolute offset has to take into account that the circle starts at an offset
        mSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density);
        mTotalDragDistance = mSpinnerOffsetEnd;
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);

        mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);

        mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
        //頭部重新整理控制元件起始位置
        moveToStart(1.0f);

        final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
        setEnabled(a.getBoolean(0, true));
        a.recycle();
    }

在構造方法中主要做了一下幾件事情:

  • 對一些常量(列如動畫時間,圓的直徑,圓的偏移量等)的設定
  • 將一個頭部重新整理控制元件加入進來
  • 建立mNestedScrollingParentHelper,mNestedScrollingChildHelper等物件,為這個檢視啟用巢狀滾動

onMeasure 方法:三件事

  • 找出目標View
  • 測量子控制元件的大小
  • 得到下拉重新整理View的Index
 @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //mTarget:手勢拖動的目標View
        if (mTarget == null) {
        //將不是頭部重新整理的View賦給mTarget
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }
        //根據測量規格測出目標View的大小
        mTarget.measure(MeasureSpec.makeMeasureSpec(
                getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
                getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
        //同上
        mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY));
        mCircleViewIndex = -1;
        // Get the index of the circleview.
        for (int index = 0; index < getChildCount(); index++) {
            if (getChildAt(index) == mCircleView) {
                mCircleViewIndex = index;
                break;
            }
        }
    }

在得到各個子控制元件的大小後,就是對各個控制元件的排版問題,也就是 onLayout()方法

  • 確定目標View的位置:child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
  • 確定重新整理控制元件的位置:mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
    (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
 @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int width = getMeasuredWidth();
        final int height = getMeasuredHeight();
        if (getChildCount() == 0) {
            return;
        }
        if (mTarget == null) {
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }
        final View child = mTarget;
        final int childLeft = getPaddingLeft();
        final int childTop = getPaddingTop();
        final int childWidth = width - getPaddingLeft() - getPaddingRight();
        final int childHeight = height - getPaddingTop() - getPaddingBottom();
        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
        int circleWidth = mCircleView.getMeasuredWidth();
        int circleHeight = mCircleView.getMeasuredHeight();
        mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
                (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
    }

因為下拉重新整理控制元件是在一開始的時候是不顯示的,所以就要考慮各個子控制元件的繪製順序,將下拉重新整理控制元件放在最後繪製,getChildDrawingOrder用於返回當前迭代子檢視的索引.就是說獲取當前正在繪製的檢視索引. 如果需要改變ViewGroup子檢視繪製的順序,則需要過載這個方法.(我試了一下,不重寫好像也沒問題)

 @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        if (mCircleViewIndex < 0) {
            return i;
        } else if (i == childCount - 1) {
            // Draw the selected child last
            return mCircleViewIndex;
        } else if (i >= mCircleViewIndex) {
            // Move the children after the selected child earlier one
            return i + 1;
        } else {
            // Keep the children before the selected child the same
            return i;
        }
    }

然後就是觸控事件分發機制;onInterceptTouchEvent():onInterceptTouchEvent是在ViewGroup裡面定義的,該方法決定了事件到底交給誰處理 。

  • 當return true時,表示ViewGroup自己來處理onTouchEvent事件,子View接收不到onTouchEvent事件
  • 當return false時,表示ViewGroup不攔截事件,直接交給子View處理

onTouchEvent:

  • onTouchEvent只有當onInterceptTouchEvent返回true的時候才執行。它根據下拉的距離,動態的修改headerView的位置,通過呼叫setTargetOffsetTopAndBottom呼叫invalidate()方法進行重繪。
 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    //一大堆根據當前狀態判斷是否攔截觸控事件的邏輯
    //就是根據是否是最後一個條目或者是第一個條目進行事件攔截
       ....
    }
@Override
    public boolean onTouchEvent(MotionEvent ev) {
        ...

            case MotionEvent.ACTION_MOVE: {
                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                    return false;
                }

                final float y = ev.getY(pointerIndex);
                startDragging(y);

                if (mIsBeingDragged) {
                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                    if (overscrollTop > 0) {
                        moveSpinner(overscrollTop);
                    } else {
                        return false;
                    }
                }
                break;
            }

        ...  

        case MotionEvent.ACTION_UP: {
                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
                    return false;
                }

                if (mIsBeingDragged) {
                    final float y = ev.getY(pointerIndex);
                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                    mIsBeingDragged = false;
                    finishSpinner(overscrollTop);
                }
                mActivePointerId = INVALID_POINTER;
                return false;
            } 
    }

onTouchEvent方法中最重要的便是moveSpinner(overscrollTop)和finishSpinner(overscrollTop)方法的呼叫

  • 獲取拖拽百分比和高度差並修正
  • 開啟動畫
  • 動態修正下拉重新整理控制元件的位置
  • 設定監聽

  • moveSpinner

 @SuppressLint("NewApi")
    private void moveSpinner(float overscrollTop) {
        mProgress.showArrow(true);
        //原始拖動距離百分比
        float originalDragPercent = overscrollTop / mTotalDragDistance;
        //原諒我的數學太差,我不知道我為什麼用下面的公式計算下拉偏移量,
        float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
        float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;
        float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;
        float slingshotDist = mUsingCustomStart ? mSpinnerOffsetEnd - mOriginalOffsetTop
                : mSpinnerOffsetEnd;
        float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2)
                / slingshotDist);
        float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
                (tensionSlingshotPercent / 4), 2)) * 2f;
        float extraMove = (slingshotDist) * tensionPercent * 2;

        int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove);


        // where 1.0f is a full circle
        if (mCircleView.getVisibility() != View.VISIBLE) {
            mCircleView.setVisibility(View.VISIBLE);
        }
        if (!mScale) {
            ViewCompat.setScaleX(mCircleView, 1f);
            ViewCompat.setScaleY(mCircleView, 1f);
        }

        if (mScale) {
            setAnimationProgress(Math.min(1f, overscrollTop / mTotalDragDistance));
        }
        if (overscrollTop < mTotalDragDistance) {
            if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA
                    && !isAnimationRunning(mAlphaStartAnimation)) {
                // Animate the alpha
                startProgressAlphaStartAnimation();
            }
        } else {
            if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {
                // Animate the alpha
                startProgressAlphaMaxAnimation();
            }
        }
        float strokeStart = adjustedPercent * .8f;
        mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));
        mProgress.setArrowScale(Math.min(1f, adjustedPercent));

        float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
        mProgress.setProgressRotation(rotation);
        setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
    }
  • finishSpinner
private void finishSpinner(float overscrollTop) {
        if (overscrollTop > mTotalDragDistance) {
        //下拉重新整理狀態的設定
            setRefreshing(true, true /* notify */);
        } else {
            // cancel refresh
            mRefreshing = false;
            mProgress.setStartEndTrim(0f, 0f);
            Animation.AnimationListener listener = null;
            if (!mScale) {
                listener = new Animation.AnimationListener() {

                    @Override
                    public void onAnimationStart(Animation animation) {
                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        if (!mScale) {
                            startScaleDownAnimation(null);
                        }
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {
                    }

                };
            }
            //這個方法是進行下拉重新整理的回覆在ANIMATE_TO_START_DURATION=200毫秒內
            animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
            mProgress.showArrow(false);
        }
    }

總結

到此我們的分析基本結束了,讓我們一起來看看做了多少事情才寫出一個下拉重新整理控制元件

  • addView() 加入下拉重新整理控制元件
  • 測量各個子view的大小
  • onLayout()對子view的位置進行確定以及確定各子view的繪製順序
  • 觸控事件的分發機制
  • 巢狀滑動實現
  • 設定回撥介面

下拉重新整理控制元件的機制我們瞭解的差不多了,下面就是我們定製自己的下拉重新整理,上拉載入控制元件了——仿UC頭條下拉重新整理佈局

ps:上面也說了樓主數學太差,那個圓的角度變化和下拉距離偏移量的關係式對樓主來說太難了,所以就搞了個假的!不多說了,來看下效果圖:

這裡寫圖片描述

這只是加深對自定義ViewGroup的理解而做的一個小Demo,下面是程式碼地址

Github>>>,大家隨便看看就行,推薦一個很酷炫的下拉重新整理第三方,樓主就是看了這位大神寫的下拉重新整理控制元件才想看看原理是怎樣的!
酷炫的下拉重新整理上拉載入控制元件》》》

  • 若有錯誤,敬請指正!!!

拼搏在技術道路上的一隻小白And成長之路

相關推薦

SwipeRefreshLayout原始碼分析+定義UC頭條重新整理Demo

首先來看SwipeRefreshLayout(以下簡稱SR)的繼承關係 NestedScrollingParent:巢狀滑動父介面 NestedScrollingChild :巢狀滑動子介面 Android 就是通過這兩個介面, 來實現 子View

Android UI 定義ListView 實現重新整理 載入更多

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

Banner+定義View+SmartRefreshLayout重新整理載入更多

仿美團開源專案整體架構和首頁其實早就完成了,前段時間家裡各種事情搞得心力交瘁,停更了一段時間。甚至一度動搖繼續這個專案的決心,因為最近在學前端,在技術的深度和廣度之間一直糾結搖擺不定。一個聲音是繼續完成這個專案,把安卓玩的更深入一些;另一個聲音是趕緊學前端吧

定義ListView實現重新整理和上載入

實現ListView的下拉重新整理和上拉載入,需要先新增headerView和footerView,通過在拖動的過程中,控制頭尾佈局的paddingTop實現。先把paddingTop設為負值,來隱藏header,在下拉的過程中,不斷改變headerView的p

定義RecyclerView實現重新整理和上載入

2)尾部佈局(上拉載入部分):refresh_recyclerview_footer.xml<span style="font-size:14px;"><?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro

Android定義View之(重新整理+側滑刪除)

以前專案中用到了一個放qq的側滑刪除的效果,結果github上一搜就copy了一個,不得不說大神們寫的真心牛逼,那個時候呢看到一個東西能用就可以了,也不管怎麼實現的,現在反過來一看,原來自定義還可以這麼玩,當然,前面專案中也因此出現了一個bug,就是我使用的是P

定義RecyclerView實現重新整理和上載入(第一種實現方式)

說明:該自定義RecyclerView只適用於layoutManager為LinearLayoutManager的情況,使用的還是RecyclerView.Adapter。效果圖使用1、編寫layout檔案<?xml version="1.0" encoding="ut

android 定義ListView實現重新整理、分頁載入、點選事件——定義控制元件學習(七)

package com.example.administrator.customerpulldownrefreshandpageload; import android.content.Context; import android.os.Handler; import android.os.Message

react-native-page-listview使用方法(定義FlatList/ListView重新整理,上載入更多,方便的實現分頁)

react-native-page-listview 對ListView/FlatList的封裝,可以很方便的分頁載入網路資料,還支援自定義下拉重新整理View和上拉載入更多的View.相容高版本FlatList和低版本ListVIew.元件會根據你使用的re

定義SwipeRefreshLayout實現上載入更多並帶系統的重新整理

/** * Created by lzy on 2017/6/6 0006. */ public class MySwipeRefreshLayout extends SwipeRefreshLayout{ private final TextView mFoo

jq定義多選列表框

多選 img 插件 國家 http 分享 class 下拉 blog 多選選擇國家插件 https://gitee.com/richard1015/dropDownList jq自定義多選下拉列表框

微信小程序-定義QQ版刷新

簡單的 做出 監聽 正常 事件 sla inf 操作 系統 最近給別個公司做技術支持,要實現微信小程序上拉刷新與下拉加載更多 微信給出的接口不怎麽友好,最終想實現效果類似QQ手機版 ,一共3種下拉刷新狀態變化,文字+圖片+背景顏色 最終實現後的效果(這裏提

C# 生成定義美化DropDownList

效果圖:ASPX程式碼: <asp:DropDownList ID="drpZhiWutSort" runat="server"></asp:DropDownList>C#程式碼: using (LeaderTripDataContext db =

再也不用擔心重新整理,上載入啦!-定義ListView對上重新整理,上載入的詳解

前言:       看過許多下拉重新整理的例子,好多大牛們的程式碼寫的很完美,讓人羨慕嫉妒恨~~~,可是,對於下拉重新整理時的手勢操作卻沒有給出詳細的解釋,當一堆堆邏輯程式碼出來的時候,對於我們這些菜鳥來說,理解起來真是讓人腦子都大了。為了解放大腦(懶得自己進行全面分析),

Spinner定義樣式,圖示

1、新建spinner_province_bg.xml <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item> <shape>

支援中文/全拼/簡拼以及定義篩選的列表

簡介 fxss-autoSelectSearch是一款jquery外掛,支援中文/全拼/簡拼等多種搜尋方式的搜尋外掛,還支援清空搜尋列表、強制指定某個搜尋框選擇某項option。 使用 首先必須引入jQuery檔案、fxss-autoSelectS

解讀Google官方SwipeRefreshLayout控制元件原始碼,帶你揭祕Android重新整理的實現原理

前言 想必大家也發現,時下的很多App都應用了這個Google出品的SwipeRefreshLayout下拉重新整理控制元件,它以Material Design風格、適用場景廣泛,簡單易用等特性而獨步江湖。但在我們使用的過程中,不可避免地會發現一些bug,或者

(四十八)c#Winform定義控制元件-按鈕

前提 入行已經7,8年了,一直想做一套漂亮點的自定義控制元件,於是就有了本系列文章。 GitHub:https://github.com/kwwwvagaa/NetWinformControl 碼雲:https://gitee.com/kwwwvagaa/net_winform_custom_contr

(四十九)c#Winform定義控制元件-框(表格)

前提 入行已經7,8年了,一直想做一套漂亮點的自定義控制元件,於是就有了本系列文章。 GitHub:https://github.com/kwwwvagaa/NetWinformControl 碼雲:https://gitee.com/kwwwvagaa/net_winform_custom_contr

Android使用SVG實現今日頭條重新整理動畫

1 SVG的全稱是Scalable Vector Graphics,叫可縮放向量圖形。它和點陣圖(Bitmap)相對,SVG不會像點陣圖一樣因為縮放而讓圖片質量下降。 2 Android