貝塞爾曲線(xiàn)下拉控件動(dòng)畫(huà)效果實(shí)現(xiàn)

導(dǎo)語(yǔ):

根據(jù)手勢(shì)做自己想要的動(dòng)畫(huà)效果呈現(xiàn)到界面,是一件超級(jí)酷炫的事情新博!閱讀本文需要你了解這幾個(gè)知識(shí)點(diǎn):

1语稠、貝塞爾曲線(xiàn)繪制方法
2、差值器之DecelerateInterpolator
3睦裳、Touch事件攔截機(jī)制
4造锅、手勢(shì)滑動(dòng)監(jiān)聽(tīng)
5廉邑、View的動(dòng)態(tài)布局
6糙箍、自定義View

一牵祟、繪制貝塞爾曲線(xiàn)

自定義WaveView诺苹,重寫(xiě)onDraw方法筝尾。
<pre><code>
@Override
protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

    //重置畫(huà)筆
    path.reset();
    path.lineTo(0, headHeight);
    //繪制貝塞爾曲線(xiàn)
    path.quadTo(getMeasuredWidth() / 2, headHeight + waveHeight, getMeasuredWidth(), headHeight);
    path.lineTo(getMeasuredWidth(), 0);
    canvas.drawPath(path, paint);
}

</pre></code>

可以看出繪制貝塞爾曲線(xiàn)用的path.quadTo方法:

quadTo(float x1, float x2, float y1, float y2)
x1,y1為控制點(diǎn)的坐標(biāo)站辉,x2,y2為終點(diǎn)坐標(biāo)值饰剥。

headHeight為繪制區(qū)域頭部矩形區(qū)域汰蓉,waveHeight為貝塞爾曲線(xiàn)區(qū)域顾孽。

WaveView的代碼如下:
<pre><code>
public class WaveView extends View {

private int waveHeight;

private int headHeight;

private Path path;

private Paint paint;

private int color;

public WaveView(Context context) {
    this(context, null, 0);
}

public WaveView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public WaveView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
}

private void init() {
    path = new Path();
    paint = new Paint();
    paint.setColor(Color.argb(150, 43, 43, 43));
    paint.setAntiAlias(true);
}

public void setColor(int color) {
    this.color = color;
    paint.setColor(color);
    invalidate();
}

public int getHeadHeight() {
    return headHeight;
}

public void setHeadHeight(int headHeight) {
    this.headHeight = headHeight;
}

public int getWaveHeight() {
    return waveHeight;
}

public void setWaveHeight(int waveHeight) {
    this.waveHeight = waveHeight;
}


@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //重置畫(huà)筆
    path.reset();
    path.lineTo(0, headHeight);
    //繪制貝塞爾曲線(xiàn)
    path.quadTo(getMeasuredWidth() / 2, headHeight + waveHeight, getMeasuredWidth(), headHeight);
    path.lineTo(getMeasuredWidth(), 0);
    canvas.drawPath(path, paint);
}

}
</pre></code>

如果定義headHeigt=100,waveHeight=200拦英,繪制出來(lái)的View如下:

Paste_Image.png

二、動(dòng)態(tài)布局

為了使下拉刷新控件適用任何布局霎冯,需要自定義一個(gè)布局,最好是繼承FrameLayout布局慷荔,因?yàn)镕rameLayout布局是疊加的拧廊。
在onAttachedToWindow方法中再新建一個(gè)FrameLayout吧碾,將下拉刷新頭部的貝塞爾控件和文案顯示控件放置里面倦春,置頂睁本。
<pre><code>
@Override
protected void onAttachedToWindow() {

super.onAttachedToWindow();

    //添加一個(gè)FrameLayout布局
    
    mFlayout = new FrameLayout(getContext());

    LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
    lp.gravity = Gravity.TOP;
    mFlayout.setLayoutParams(lp);
    this.addView(mFlayout);

    //頭部貝塞爾控件和文案顯示控件
    View refreshView = LayoutInflater.from(getContext()).inflate(R.layout.fresh_layout, null);
    txtRefresh = (TextView) refreshView.findViewById(R.id.txtRefresh);
    waveView = (WaveView) refreshView.findViewById(R.id.wave);
    waveView.setWaveHeight(WAVE_HEIGHT);
    waveView.setHeadHeight(WAVE_HEAD_HEIGHT);
    waveView.invalidate();
    mFlayout.addView(refreshView);

    //獲取子控件
    childView = getChildAt(0);
}

</pre></code>

三、Touch事件攔截

下拉刷新凡泣,事件攔截有如下兩種情況:

1骂维、正在下拉中
2贺纲、子控件不能往上滑動(dòng)

判斷是否正在下拉可以用一個(gè)布爾值搞定
判斷子控件是否能往上滑動(dòng)需要我們?nèi)?xiě)一個(gè)方法
<pre><code>
/**
* 判斷是否可以上拉
* @return
*/
private boolean canChildScrollUp() {
if(childView instanceof AbsListView) {
AbsListView absLv = (AbsListView) childView;
return absLv.getChildCount() > 0
&& (absLv.getFirstVisiblePosition() > 0 || absLv.getChildAt(0).getTop() < absLv.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(childView, -1);
}
}
</pre></code>

這個(gè)方法可以用來(lái)判斷View是否可以往上滑動(dòng)侮措,這里講View分成兩類(lèi)福铅,一類(lèi)是列表ListView控件滑黔,一類(lèi)是普通的View類(lèi)略荡。ListView控件判斷是否有孩子汛兜,并且第一孩子需要在界面呈現(xiàn)粥谬,并且第一孩子的頂部坐標(biāo)要小于ListView控件的paddingTop值漏策。普通View類(lèi)可以根據(jù)sdk自帶的canScrollVertically去判斷,有興趣可以去看看源碼储矩。

該方法為了兼容更多Android系統(tǒng)即硼,建議修改成下面的代碼:
<pre><code>
/**
* 用來(lái)判斷是否可以上拉
*
* @return boolean
*/
public boolean canChildScrollUp() {
if (mChildView == null) {
return false;
}
if (Build.VERSION.SDK_INT < 14) {
if (mChildView instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mChildView;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(mChildView, -1) || mChildView.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mChildView, -1);
}
}
</pre></code>

然后重寫(xiě)onInterceptTouchEvent方法只酥,完善Touch事件攔截
<pre><code>
public boolean onInterceptTouchEvent(MotionEvent ev) {

    if(mIsRefreshing) {
        return true; //如果下拉刷新,則攔截
    }
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mTouchY =  ev.getY();
            mCurrentY = mTouchY;
        case MotionEvent.ACTION_MOVE:
            float currentY = ev.getY();
            float y = currentY - mTouchY; //計(jì)算當(dāng)前滑動(dòng)距離
            if(y > 0 && !canChildScrollUp()) {
                return true;
            }
            break;
    }
    return super.onInterceptTouchEvent(ev);
}

</pre></code>

手勢(shì)滑動(dòng)監(jiān)聽(tīng)

監(jiān)聽(tīng)手勢(shì)滑動(dòng)以及手勢(shì)取消兩個(gè)過(guò)程,即ACTION_MOVE和ACTION_CANCLE | ACTION_UP瓮增。
滑動(dòng)過(guò)程主要根據(jù)滑動(dòng)距離做動(dòng)畫(huà)效果,以及判斷下拉刷新?tīng)顟B(tài)拳恋∶耍滑動(dòng)結(jié)束主要處理子控件的位置回歸何處。當(dāng)然轰驳,當(dāng)onInterceptTouchEvent方法返回true弟灼,表示當(dāng)前FrameLayout攔截Touch事件田绑,觸摸事件就會(huì)交給onTouch處理俺陋,所以重寫(xiě)onTouch方法如下:
<pre><code>
public boolean onTouchEvent(MotionEvent event) {
if(mIsRefreshing) {
return super.onTouchEvent(event);
}

    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE:
            float currentY = event.getY();
            float y = currentY - mTouchY;
            y = Math.min(WAVE_HEIGHT * 2, y);
            y = Math.max(0, y);
            //計(jì)算滑動(dòng)距離
            float offsetY = decelerInterpolator.getInterpolation(y / WAVE_HEIGHT / 2) * y / 2;
            //子控件移動(dòng)同樣距離
            childView.setTranslationY(offsetY);

            //控件高度
            mFlayout.getLayoutParams().height = (int) offsetY;
            mFlayout.requestLayout();

            //貝塞爾曲線(xiàn)
            float fraction = offsetY / WAVE_HEAD_HEIGHT;
            waveView.setHeadHeight((int) (WAVE_HEAD_HEIGHT * limitValue(1, fraction)));
            waveView.setWaveHeight((int) (WAVE_HEIGHT * Math.max(0, fraction - 1)));
            waveView.invalidate();

            if(WAVE_HEAD_HEIGHT > WAVE_HEAD_HEIGHT * limitValue(1, fraction)) {
                txtRefresh.setText("下拉刷新");
            } else {
                txtRefresh.setText("釋放刷新");
            }

            return true;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            //如果滑動(dòng)距離大于貝塞爾頭部矩形區(qū)域高度,子控件回到矩形區(qū)域高度位置苔可,否則子控件置頂
            if(childView.getTranslationY() >= WAVE_HEAD_HEIGHT) {
                setChildViewTransY(WAVE_HEAD_HEIGHT);
            } else {
                setChildViewTransY(0);
            }
            return true;
    }
    return super.onTouchEvent(event);
}

</pre></code>

差值器DecelerateInterpolator

該差值器實(shí)現(xiàn)的效果:在動(dòng)畫(huà)開(kāi)始的地方快然后慢缴挖。這里就不再贅述其他差值器了,感興趣可以去看看差值器的源碼焚辅,需要懂些數(shù)學(xué)公式映屋。
該下拉刷新控件兩個(gè)地方用到DecelerateInterpolator差值器,下拉刷新的過(guò)程以及刷新完成后的控件位置回歸過(guò)程同蜻。
<pre><code>
/**
* 控件滑動(dòng)結(jié)束后回歸動(dòng)畫(huà)
* @param values
*/
private void setChildViewTransY(float... values) {
ObjectAnimator ani = ObjectAnimator.ofFloat(childView, View.TRANSLATION_Y, values);
ani.setInterpolator(new DecelerateInterpolator());
ani.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int height = (int) childView.getTranslationY();
mFlayout.getLayoutParams().height = height;
mFlayout.requestLayout();
}
});
ani.start();
}
</pre></code>

運(yùn)行效果

源碼

<pre><code>
public class WaveFrameLayout extends FrameLayout {

FrameLayout mFlayout;
private boolean mIsRefreshing;//刷新的狀態(tài)
private float mTouchY;//當(dāng)前觸摸位置
private float mCurrentY;//當(dāng)前位置
private View childView;
private WaveView waveView;
TextView txtRefresh;

private final int WAVE_HEIGHT = 200;
private final int WAVE_HEAD_HEIGHT = 100;
private DecelerateInterpolator decelerInterpolator;

public WaveFrameLayout(Context context) {
    super(context);
    init(context);
}

public WaveFrameLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
}


public WaveFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}


private void init(Context context) {
    if (isInEditMode()) {
        return;
    }
    decelerInterpolator = new DecelerateInterpolator(10);
}

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    //添加一個(gè)FrameLayout布局
    mFlayout = new FrameLayout(getContext());
    LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
    lp.gravity = Gravity.TOP;
    mFlayout.setLayoutParams(lp);
    this.addView(mFlayout);

    //頭部貝塞爾控件和文案顯示控件
    View refreshView = LayoutInflater.from(getContext()).inflate(R.layout.fresh_layout, null);
    txtRefresh = (TextView) refreshView.findViewById(R.id.txtRefresh);
    waveView = (WaveView) refreshView.findViewById(R.id.wave);
    waveView.setWaveHeight(WAVE_HEIGHT);
    waveView.setHeadHeight(WAVE_HEAD_HEIGHT);
    waveView.invalidate();
    mFlayout.addView(refreshView);

    //獲取子控件
    childView = getChildAt(0);
}

/**
 * 判斷是否可以上拉
 * @return
 */
private boolean canChildScrollUp() {
    if(childView instanceof AbsListView) {
        AbsListView absLv = (AbsListView) childView;
        return absLv.getChildCount() > 0
                && (absLv.getFirstVisiblePosition() > 0 || absLv.getChildAt(0).getTop() < absLv.getPaddingTop());
    } else {
        return ViewCompat.canScrollVertically(childView, -1);
    }
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if(mIsRefreshing) {
        return true; //如果下拉刷新棚点,則攔截
    }
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mTouchY =  ev.getY();
            mCurrentY = mTouchY;
        case MotionEvent.ACTION_MOVE:
            float currentY = ev.getY();
            float y = currentY - mTouchY; //計(jì)算當(dāng)前滑動(dòng)距離
            if(y > 0 && !canChildScrollUp()) {
                return true;
            }
            break;
    }
    return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    if(mIsRefreshing) {
        return super.onTouchEvent(event);
    }

    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE:
            float currentY = event.getY();
            float y = currentY - mTouchY;
            y = Math.min(WAVE_HEIGHT * 2, y);
            y = Math.max(0, y);
            //計(jì)算滑動(dòng)距離
            float offsetY = decelerInterpolator.getInterpolation(y / WAVE_HEIGHT / 2) * y / 2;
            //子控件移動(dòng)同樣距離
            childView.setTranslationY(offsetY);

            //控件高度
            mFlayout.getLayoutParams().height = (int) offsetY;
            mFlayout.requestLayout();

            //貝塞爾曲線(xiàn)
            float fraction = offsetY / WAVE_HEAD_HEIGHT;
            waveView.setHeadHeight((int) (WAVE_HEAD_HEIGHT * limitValue(1, fraction)));
            waveView.setWaveHeight((int) (WAVE_HEIGHT * Math.max(0, fraction - 1)));
            waveView.invalidate();

            if(WAVE_HEAD_HEIGHT > WAVE_HEAD_HEIGHT * limitValue(1, fraction)) {
                txtRefresh.setText("下拉刷新");
            } else {
                txtRefresh.setText("釋放刷新");
            }

            return true;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            //如果滑動(dòng)距離大于貝塞爾頭部矩形區(qū)域高度瘫析,子控件回到矩形區(qū)域高度位置,否則子控件置頂
            if(childView.getTranslationY() >= WAVE_HEAD_HEIGHT) {
                setChildViewTransY(WAVE_HEAD_HEIGHT);
            } else {
                setChildViewTransY(0);
            }
            return true;
    }
    return super.onTouchEvent(event);
}

/**
 * 控件滑動(dòng)結(jié)束后回歸動(dòng)畫(huà)
 * @param values
 */
private void setChildViewTransY(float... values) {
    ObjectAnimator ani = ObjectAnimator.ofFloat(childView, View.TRANSLATION_Y, values);
    ani.setInterpolator(new DecelerateInterpolator());
    ani.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            int height = (int) childView.getTranslationY();
            mFlayout.getLayoutParams().height = height;
            mFlayout.requestLayout();
        }
    });
    ani.start();
}


/**
 * 限定值
 */
public float limitValue(float a, float b) {
    float valve = 0;
    final float min = Math.min(a, b);
    final float max = Math.max(a, b);
    valve = valve > min ? valve : min;
    valve = valve < max ? valve : max;
    return valve;
}

}
</pre></code>

布局代碼

<pre><code>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="下拉控件"
android:textColor="@color/colorAccent" />

<WaveFrameLayout
    android:id="@+id/waveFlayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/txtShow"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:text="hello world!" />
        </LinearLayout>

    </ScrollView>

</WaveFrameLayout>

</LinearLayout>
</pre></code>
布局的格式調(diào)不來(lái),注意一點(diǎn)就好,WaveView里面嵌套ScrollView或ListView,才能響應(yīng)滑動(dòng)監(jiān)聽(tīng)愈犹。后續(xù)加入事件監(jiān)聽(tīng)勋锤,下拉完成后的后續(xù)操作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末窑滞,一起剝皮案震驚了整個(gè)濱河市聊训,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌挂滓,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瑟蜈,死亡現(xiàn)場(chǎng)離奇詭異夷都,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)氯窍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)布隔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)哀军,“玉大人,你說(shuō)我怎么就攤上這事毯侦】蘧福” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)铺坞。 經(jīng)常有香客問(wèn)我擒滑,道長(zhǎng)库车,這世上最難降的妖魔是什么川无? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任疹味,我火速辦了婚禮仅叫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘糙捺。我一直安慰自己诫咱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布洪灯。 她就那樣靜靜地躺著坎缭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪签钩。 梳的紋絲不亂的頭發(fā)上掏呼,一...
    開(kāi)封第一講書(shū)人閱讀 52,268評(píng)論 1 309
  • 那天,我揣著相機(jī)與錄音边臼,去河邊找鬼哄尔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛柠并,可吹牛的內(nèi)容都是我干的岭接。 我是一名探鬼主播富拗,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鸣戴!你這毒婦竟也來(lái)了啃沪?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤窄锅,失蹤者是張志新(化名)和其女友劉穎创千,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體入偷,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡追驴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了疏之。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片殿雪。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖锋爪,靈堂內(nèi)的尸體忽然破棺而出丙曙,到底是詐尸還是另有隱情,我是刑警寧澤其骄,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布亏镰,位于F島的核電站,受9級(jí)特大地震影響拯爽,放射性物質(zhì)發(fā)生泄漏索抓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一毯炮、第九天 我趴在偏房一處隱蔽的房頂上張望纸兔。 院中可真熱鬧,春花似錦否副、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至奈揍,卻和暖如春曲尸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背男翰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工另患, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛾绎。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓昆箕,卻偏偏與公主長(zhǎng)得像鸦列,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鹏倘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

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