可以下拉縮放HeaderView的ListView:PullToZoomInListView

lkoky 8年前發布 | 9K 次閱讀 Android開發 移動開發

來自: http://www.jcodecraeer.com//a/anzhuokaifa/androidkaifa/2014/0627/1625.html


下面這種效果在ios應用中很常見:

iOS / iPhone 下拉列表或者ScrollView,放大頂部的圖片。類似tweetbot app的個人頁面效果。

其實在android中也有不少應用實現了這種效果,比如知乎日報(新版本好像去掉了),但是我覺得做的最好的還是“最美應用”。于是將最美應用的apk下載下來,用apktool反編譯出其xml文件,發現它用的是自定義的一個ListView控件:

com.brixd.android.utils.ui.PullToZoomExpandableListView,在網上搜不到相關信息,顯然是自己寫的了,于是再用dex2jar將最美應用的classes.dex反編譯成java代碼,當然編譯出來的只是其混淆過的代碼,但是仔細分析一般還是能夠看出一些有用的東西,很多第三方的開源代碼我都是在反編譯別人的代碼中知道的。

不幸的是雖然大致能從反編譯的代碼中看出其結構,也找出了PullToZoomExpandableListView這個類(其實是兩個還有個是PullToZoomListView,估計這個才是最符合我需求的),但是PullToZoomListView中處理手勢的關鍵代碼完全混淆了,放棄,只能尋找他法。

一般遇到這種情況我都想到github和stackoverflow,特別是github上,最近關于android的東西越來越完善,在github上搜listview,總算找到了一個完全符合我需求的ListView,看了代碼才發現,最美應用的PullToZoomListView其實90%的代碼和它相同,實現原理則是100%相同。突然覺得最美應用有點不厚道,用了開源的代碼都改成自己的了,不過很多人都是這樣的。

github地址如下:https://github.com/matrixxun/PullToZoomInListView

public boolean onTouchEvent(MotionEvent paramMotionEvent) {
        Log.d("mmm", "" + (0xFF & paramMotionEvent.getAction()));
        switch (0xFF & paramMotionEvent.getAction()) {
        case 4:
        case 0:
            if (!this.mScalingRunnalable.mIsFinished) {
                this.mScalingRunnalable.abortAnimation();
            }
            this.mLastMotionY = paramMotionEvent.getY();
            this.mActivePointerId = paramMotionEvent.getPointerId(0);
            this.mMaxScale = (this.mScreenHeight / this.mHeaderHeight);
            this.mLastScale = (this.mHeaderContainer.getBottom() / this.mHeaderHeight);
            break;
        case 2:
        .........
}

我將這些數字改成了能看懂的形式如 MotionEvent.ACTION_MOVE。同時將paramMotionEvent這個參數名改成了比較熟悉的ev,paramMotionEvent作為一個動作事件太長了,看起來真不舒服,其他地方未做任何修改。我修改過的代碼放到了百度網盤:

http://pan.baidu.com/s/1hqrDyoW


其實PullToZoomListView的實現原理很簡單主要是在case MotionEvent.ACTION_MOVE:代碼段中判斷向下滑動的偏移量,根據這個來改變listview headerView內容區域的高度,并且在手指放開的那一刻,停止縮放,啟用一個動畫來使HeaderView平滑的恢復到放大之前的狀態。

下面是縮放過程的關鍵代碼:

case MotionEvent.ACTION_MOVE:
    Log.d("mmm", "mActivePointerId" + mActivePointerId);
    int j = ev.findPointerIndex(this.mActivePointerId);
    if (j == -1) {
        Log.e("PullToZoomListView", "Invalid pointerId="
                + this.mActivePointerId + " in onTouchEvent");
    } else {
        if (this.mLastMotionY == -1.0F)
            this.mLastMotionY = ev.getY(j);
        if (this.mHeaderContainer.getBottom() >= this.mHeaderHeight) {
            ViewGroup.LayoutParams localLayoutParams = this.mHeaderContainer
                    .getLayoutParams();
            float f = ((ev.getY(j) - this.mLastMotionY + this.mHeaderContainer
                    .getBottom()) / this.mHeaderHeight - this.mLastScale)
                    / 2.0F + this.mLastScale;
            if ((this.mLastScale <= 1.0D) && (f < this.mLastScale)) {
                localLayoutParams.height = this.mHeaderHeight;
                this.mHeaderContainer
                        .setLayoutParams(localLayoutParams);
                return super.onTouchEvent(ev);
            }
            this.mLastScale = Math.min(Math.max(f, 1.0F),
                    this.mMaxScale);
            localLayoutParams.height = ((int) (this.mHeaderHeight * this.mLastScale));
            if (localLayoutParams.height < this.mScreenHeight)
                this.mHeaderContainer
                        .setLayoutParams(localLayoutParams);
            this.mLastMotionY = ev.getY(j);
            return true;
        }
        this.mLastMotionY = ev.getY(j);
    }
    break;

其中

localLayoutParams.height = ((int) (this.mHeaderHeight * this.mLastScale));

就是根據滑動偏移計算出新的高度,mLastScale是一個系數,只相對于原始高度的比值,mLastScale是根據垂直偏移計算出來的,其實這個mLastScale具體該怎么計算取決于你自己的想法,我根據我自己的想法也寫了個方法,也能得到縮放效果,只是沒他這個效果好。

縮放之后如果手指放開,需要平滑回復原始高度:

case MotionEvent.ACTION_UP:
    reset();
    endScraling();
    break;

reset的代碼如下

private void reset() {
    this.mActivePointerId = -1;
    this.mLastMotionY = -1.0F;
    this.mMaxScale = -1.0F;
    this.mLastScale = -1.0F;
}

這個只是恢復一些初始值,但是真正實現平滑過渡是在endScraling過程中,endScraling()的實現如下:

private void endScraling() {
    if (this.mHeaderContainer.getBottom() >= this.mHeaderHeight)
        Log.d("mmm", "endScraling");
    this.mScalingRunnalable.startAnimation(200L);
}

交給了mScalingRunnalablemScalingRunnalable是一個內部類:

class ScalingRunnalable implements Runnable {
    long mDuration;
    boolean mIsFinished = true;
    float mScale;
    long mStartTime;
    ScalingRunnalable() {
    }
    public void abortAnimation() {
        this.mIsFinished = true;
    }
    public boolean isFinished() {
        return this.mIsFinished;
    }
    public void run() {
        float f2;
        ViewGroup.LayoutParams localLayoutParams;
        if ((!this.mIsFinished) && (this.mScale > 1.0D)) {
            float f1 = ((float) SystemClock.currentThreadTimeMillis() - (float) this.mStartTime)
                    / (float) this.mDuration;
            f2 = this.mScale - (this.mScale - 1.0F)
                    * PullToZoomListView.sInterpolator.getInterpolation(f1);
            localLayoutParams = PullToZoomListView.this.mHeaderContainer
                    .getLayoutParams();
            if (f2 > 1.0F) {
                Log.d("mmm", "f2>1.0");
                localLayoutParams.height = PullToZoomListView.this.mHeaderHeight;
                ;
                localLayoutParams.height = ((int) (f2 * PullToZoomListView.this.mHeaderHeight));
                PullToZoomListView.this.mHeaderContainer
                        .setLayoutParams(localLayoutParams);
                PullToZoomListView.this.post(this);
                return;
            }
            this.mIsFinished = true;
        }
    }
    public void startAnimation(long paramLong) {
        this.mStartTime = SystemClock.currentThreadTimeMillis();
        this.mDuration = paramLong;
        this.mScale = ((float) (PullToZoomListView.this.mHeaderContainer
                .getBottom()) / PullToZoomListView.this.mHeaderHeight);
        this.mIsFinished = false;
        PullToZoomListView.this.post(this);
    }
}

我覺得難點不在于如何改變mHeaderContainer的高度吧,難在如何讓一個View按一定的時間曲線改變屬性,這其實是屬性動畫該做的事,但是這里沒有用屬性動畫,顯然作者對View的動畫很熟悉,我覺得這里作者用自己的方法實現了屬性動畫, ScalingRunnalable在startAnimation中調用了PullToZoomListView.this.post(this);不懂得可以搜索View.post(Runnable),post調用ScalingRunnalable的run方法,而ScalingRunnalable run方法中再次調用了post,就這樣不斷的更新UI,直到達到一定的條件退出這個循環(這里這個條件是if((!this.mIsFinished) && (this.mScale > 1.0D))),這里的關鍵點是每次執行run的時候mHeaderContainer的高度究竟該變化多少,

f2 = this.mScale - (this.mScale - 1.0F)
                    * PullToZoomListView.sInterpolator.getInterpolation(f1);

PullToZoomListView.sInterpolator.getInterpolation(f1)給了我們答案,但是我也沒太明白這個getInterpolation的含義,以后遇到這樣的問題依葫蘆畫瓢就是了。

 本文由用戶 lkoky 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!