Android拖拽窗市、回彈布局

這一次拆解的是今日頭條的關(guān)注頁面:點擊關(guān)注的頭像會彈出一個文章列表栗菜。在邊界拖拽會出現(xiàn)關(guān)閉提示。這次同時實現(xiàn)了Android端和IOS端的效果像棘。

先講解Android端的實現(xiàn)吧稽亏,畢竟我是個Android開發(fā)仔呀

效果如下圖:


Android端

彈出來的頁面可以左右切換,每個頁面是單獨的列表缕题,能上下滑動截歉,所以這里直接用viewPager+recycelrView實現(xiàn)。
當viewPager不能左右滑動的時候烟零,移動整個viewPager瘪松,出現(xiàn)文字提示咸作,當滑動距離超過閾值時,文字改變宵睦。
當手指松開時记罚,若滑動距離未到達閾值,回彈壳嚎;否則結(jié)束頁面桐智。
同樣,當recyclerView在頂部不能滑動時诬辈,移動recyclerView酵使,出現(xiàn)提示,后續(xù)跟viewPager一致故不再贅述焙糟。

ReBoundLayout

這里的回彈我自定義了一個回彈布局口渔,下面介紹一下回彈布局的幾個重要方法:
onInterceptTouchEvent()

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                //記錄坐標
                break;
            case MotionEvent.ACTION_MOVE:
                int difX = (int) (ev.getX() - mDownX);
                int difY = (int) (ev.getY() - mDownY);
                if (orientation == LinearLayout.HORIZONTAL) {
                        .....
                    if (水平滑動) {
                        if (!innerView.canScrollHorizontally(-1) && difX > 0) {
                            //右拉到邊界
                            return true;
                        }
                        if (!innerView.canScrollHorizontally(1) && difX < 0) {
                            //左拉到邊界
                            return true;
                        }
                    }
                } else {
                     ......
                    if (豎直滑動) {
                        if (!innerView.canScrollVertically(-1) && difY > 0) {
                            //下拉到邊界
                            return true;
                        }
                        if (!innerView.canScrollVertically(1) && difY < 0) {
                            //上拉到邊界
                            return true;
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
               ......重置變量
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

當控件方向為橫向且滑動為水平滑動時,檢測innerView能否在該方向上滑動穿撮;若不能缺脉,則攔截事件,交給自身處理(縱向同理)悦穿。
攔截事件后攻礼,在onTouchEvent()進行處理,實現(xiàn)移動和回彈栗柒。

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                if (orientation == LinearLayout.HORIZONTAL) {
                    int difX = (int) ((event.getX() - mDownX) / resistance);
                    boolean isRebound = false;
                    if (!innerView.canScrollHorizontally(-1) && difX > 0) {
                        //右拉到邊界
                        isRebound = true;
                    } else if (!innerView.canScrollHorizontally(1) && difX < 0) {
                        //左拉到邊界
                        isRebound = true;
                    }
                    if (isRebound) {
                        //移動和回調(diào)
                        return true;
                    }
                } else {
                    int difY = (int) ((event.getY() - mDownY) / resistance);
                    boolean isRebound = false;
                    if (!innerView.canScrollVertically(-1) && difY > 0) {
                        //下拉到邊界
                        isRebound = true;
                    } else if (!innerView.canScrollVertically(1) && difY < 0) {
                        //上拉到邊界
                        isRebound = true;
                    }
                    if (isRebound) {
                        //移動和回調(diào)
                        return true;
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (orientation == LinearLayout.HORIZONTAL) {
                    int difX = (int) innerView.getTranslationX();
                    if (difX != 0) {
                        if (Math.abs(difX) <= resetDistance || isNeedReset) {
                            innerView.animate().translationX(0).setDuration(mDuration).setInterpolator(mInterpolator);
                        }
                        //回調(diào)
                    }
                } else {
                    int difY = (int) innerView.getTranslationY();
                    if (difY != 0) {
                        if (Math.abs(difY) <= resetDistance || isNeedReset) {
                            innerView.animate().translationY(0).setDuration(mDuration).setInterpolator(mInterpolator);
                        }
                        //回調(diào)
                    }
                }
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

MOVE事件
利用setTranslationX()setTranslationY()改變innerView的位置礁扮,同時將滑動距離和方向通過接口回調(diào)到外面。
UP事件
判斷滑動距離是否小于閾值瞬沦,小于則執(zhí)行回彈動畫太伊;同時回調(diào)到外面。
以上就是回彈布局的簡單實現(xiàn)逛钻,主要是對滑動事件進行攔截處理僚焦,如果不清楚事件傳遞機制可以到這里查看。
布局有3個自定義屬性

<declare-styleable name="ReBoundLayout">
        <attr name="reBoundOrientation" format="enum">
            <enum name="horizontal" value="0" />
            <enum name="vertical" value="1" />
        </attr>
        <attr name="resistance" format="float" />
        <attr name="reBoundDuration" format="integer" />
</declare-styleable>

分別是:回彈方向曙痘、阻力系數(shù)芳悲、回彈時間,剩余屬性可以調(diào)用set()方法修改边坤。


好了名扛,現(xiàn)在回彈實現(xiàn)了,接下來就是將文字提示加上茧痒,結(jié)束動畫加上肮韧。這里有一點需要注意的是:demo中使用的是reBoundLayout+viewPager+fragment(reBoundLayout+recyclerView)的結(jié)構(gòu)實現(xiàn)的。而文字是跟viewPager同一層級的,所以需要把fragment的回調(diào)回調(diào)到activity里(也可以getActivity()獲取對應(yīng)的文字控件)惹苗,詳見代碼。
以下是回調(diào)的偽代碼:

 @Override
    public void onDistanceChange(int distance, int direction) {
        switch (direction) {
            case DIRECTION_LEFT:
                if (distance > showTipDistance) {
                    //文字改變耸峭,移動
                } else {
                    rightTip.setVisibility(View.GONE);
                }
                break;
            case DIRECTION_RIGHT:
                if (distance > showTipDistance) {
                   //文字改變桩蓉,移動
                } else {
                    leftTip.setVisibility(View.GONE);
                }
                break;
            case DIRECTION_UP:
                break;
            case DIRECTION_DOWN:
                //fragment的回調(diào)會走到這里
                if (distance > showTipDistance) {
                    //文字改變,移動
                } else {
                    topTip.setVisibility(View.GONE);
                }
                break;
            default:
                break;
        }
    }

    @Override
    public void onFingerUp(int distance, int direction) {
        switch (direction) {
            case DIRECTION_LEFT:
                if (distance > mResetDistance) {
                    //結(jié)束頁面
                } else {
                    //文字重置
                }
                break;
            case DIRECTION_RIGHT:
                if (distance > mResetDistance) {
                   //結(jié)束頁面
                } else {
                    //文字重置
                }
                break;
            case DIRECTION_DOWN:
                if (distance > mResetDistance) {
                  //結(jié)束頁面
                } else {
                    //文字重置
                }
                break;
            default:
                break;
        }
    }

大功告成劳闹,Android端的效果比較簡單院究,實現(xiàn)起來也比較容易。

IOS端效果復(fù)雜一丟丟本涕,大家留心看业汰。

效果如下:


IOS端

當頁面不能拖動時(右滑、左滑菩颖、下滑)样漆,view的位置開始改變,并且整個頁面會縮小成一個圓晦闰;當松手時距離大于閾值放祟,view縮小為一個圓并平移到進入的那個圓位置,結(jié)束當前頁面呻右;否則回彈(demo中只給出一個圓跪妥,若需實現(xiàn)頭條的效果,只需更改對應(yīng)Point點得坐標即可)声滥。
同樣眉撵,自定義一個布局進行滑動事件的處理,至于整個頁面的縮小變圓落塑,這里通過裁剪畫布的方式去實現(xiàn)(圓心固定在屏幕中央)纽疟,也可以通過別的方法(Xfermode)去實現(xiàn)同樣的效果,有興趣的朋友自行探索芜赌。

PS:如果想圓心跟隨手指移動仰挣,需要增加以下計算:圓最大半徑、圓可移動距離與半徑變化關(guān)系

DragZoomLayout

關(guān)鍵變量:

  • mMinRadius 圓最小半徑
  • mMaxRadius 圓最大半徑
  • mRadius 當前半徑
  • mTranslationX 當前X移動距離
  • mTranslationY 當前Y移動距離

事件攔截跟ReBoundLayout一致缠沈,所以不贅述膘壶,主要看看滑動事件的處理

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                int difX = (int) ((event.getX() - mDownX) / resistance);
                int difY = (int) ((event.getY() - mDownY) / resistance);
                if (orientation == LinearLayout.HORIZONTAL) {
                    boolean needDrag = false;
                    if (!innerView.canScrollHorizontally(-1) && difX > 0) {
                        //右啦到邊界
                        needDrag = true;
                    } else if (!innerView.canScrollHorizontally(1) && difX < 0) {
                        //左拉到邊界
                        needDrag = true;
                    }
                    if (needDrag) {
                        //半徑計算
                        mTranslationX = difX;
                        mTranslationY = difY;
                        invalidate();
                        //回調(diào)
                        return true;
                    }
                } else {
                    if (!innerView.canScrollVertically(-1) && difY > 0) {
                        //下拉到邊界
                        //回調(diào)
                        return true;
                    } else if (!innerView.canScrollVertically(1) && difY < 0) {
                        //上啦到邊界
                        innerView.setTranslationY(difY);
                        return true;
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (orientation == LinearLayout.HORIZONTAL) {
                    //水平
                    if (Math.abs(mTranslationX) >= resetDistance) {
                        //回調(diào)
                    } else {
                        //重置狀態(tài)
                    }
                } else {
                    //豎直
                    if (innerView.getTranslationY() < 0) {
                        innerView.animate().setDuration(mDuration).translationY(0).setInterpolator(mInterpolator);
                    } else {
                        //回調(diào)
                    }
                }
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

這里跟ReBoundLayout有以下幾點區(qū)別:

  • 通過裁剪畫布的方式達到view縮小成圓的效果
  • 通過移動畫布達到移動view的效果(setTranslation會觸發(fā)view的重繪,同時改變x跟y洲愤,會調(diào)用2次颓芭,而修改畫布大小又需要重繪,調(diào)用次數(shù)太多柬赐,因此不使用該方式)
  • 下滑跟左右滑動一樣亡问,縮小、移動的都是最外層的DragZoomLayout(這樣視覺效果最好,而且能統(tǒng)一處理)州藕;上滑只做移動和回彈束世。

PS:DragZoomLayout一定要設(shè)置背景,不然調(diào)用invalidate()會無效床玻;上下滑動的mTranslationX毁涉、mTranslationY一直都是0(因為下滑我們已經(jīng)回調(diào)給最層的DragZoomLayout),所以在ACTION_UP锈死、ACTION_CANCEL事件贫堰,豎直方向回調(diào)時是使用當前事件的x、y跟點擊的x待牵、y相減的值去回調(diào)其屏。

布局繪制
 @Override
    protected void onDraw(Canvas canvas) {
        if (Math.abs(mTranslationX) > mLargeX) {
            mTranslationX = mTranslationX > 0 ? mLargeX : -mLargeX;
        }
        if (Math.abs(mTranslationY) > mLargeY) {
            mTranslationY = mTranslationY > 0 ? mLargeY : -mLargeY;
        }
        canvas.translate(mTranslationX, mTranslationY);
        mPath.reset();
        mPath.addCircle(mPoint.x, mPoint.y, mRadius, Path.Direction.CCW);
        canvas.clipPath(mPath);
        super.onDraw(canvas);
    }

進行了一些位置和半徑的限制。
布局完成缨该,接下來處理頁面間的接口回調(diào)及結(jié)束動畫


動畫的計算有一點點麻煩偎行,數(shù)學(xué)不好的同學(xué)請多看幾遍,還是不懂的趁著過年回高中找數(shù)學(xué)老師要回學(xué)費吧压彭。


先來看沒有移動畫布的情況:

啟動頁面時睦优,通過getLocationOnScreen()獲取進入時的坐標,退出時的坐標通過最外層的dragLayout的坐標加上寬高的一半壮不,再減去圓的最小半徑得到汗盘,最后通過這2個差值進行平移。
那么有平移并且半徑未到最小的情況也可以通過這種方式計算:

我們已經(jīng)有一個translationX了询一,那可以計算出目標的translationX隐孽,然后使用ValueAnimator不斷去改變它進行重繪,得到一個平移效果(translationY同理)健蕊。那這個值要怎么得到呢菱阵?上面已經(jīng)說了怎么計算了,沒看懂的再看一遍缩功∏缂埃看幾遍還是不懂的,回去找老師要學(xué)費吧嫡锌。

至于進入動畫原理相同虑稼,只是反過來執(zhí)行罷了,這里不再贅述势木,詳見代碼蛛倦。
有更好實現(xiàn)方式的歡迎下方留言討論,有bug或者疑問的也可以留言啦桌,有空會回復(fù)的溯壶。
由于篇幅關(guān)系,一些細小的地方?jīng)]有提及,有興趣的朋友可以自行查看且改。
最后奉上源碼;


這是年前最后一篇博客了验烧,今年立的flag好像都沒有實現(xiàn),跟大佬的差距還是那么大又跛,Bug仔仍需努力呀噪窘。


9102沖鴨
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市效扫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌直砂,老刑警劉巖菌仁,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異静暂,居然都是意外死亡济丘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門洽蛀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摹迷,“玉大人,你說我怎么就攤上這事郊供∠康铮” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵驮审,是天一觀的道長鲫寄。 經(jīng)常有香客問我,道長疯淫,這世上最難降的妖魔是什么地来? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮熙掺,結(jié)果婚禮上未斑,老公的妹妹穿的比我還像新娘。我一直安慰自己币绩,他們只是感情好蜡秽,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著类浪,像睡著了一般载城。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上费就,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天诉瓦,我揣著相機與錄音,去河邊找鬼。 笑死睬澡,一個胖子當著我的面吹牛固额,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播煞聪,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼斗躏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了昔脯?” 一聲冷哼從身側(cè)響起啄糙,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎云稚,沒想到半個月后隧饼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡静陈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年燕雁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鲸拥。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡拐格,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刑赶,到底是詐尸還是另有隱情捏浊,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布撞叨,位于F島的核電站呛伴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏谒所。R本人自食惡果不足惜热康,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望劣领。 院中可真熱鬧姐军,春花似錦、人聲如沸尖淘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽村生。三九已至惊暴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間趁桃,已是汗流浹背辽话。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工肄鸽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人油啤。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓典徘,卻偏偏與公主長得像益咬,于是被迫代替她去往敵國和親幽告。 傳聞我的和親對象是個殘疾皇子梅鹦,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

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

  • 之前發(fā)過一個帖子蒿讥,但是那個帖子有點問題我就重新發(fā)一個吧,下面的源碼是我從今年開始不斷整理源碼區(qū)和其他網(wǎng)站上的安卓例...
    passiontim閱讀 21,901評論 181 334
  • 一芋绸、簡歷準備 1担敌、個人技能 (1)自定義控件全封、UI設(shè)計、常用動畫特效 自定義控件 ①為什么要自定義控件行楞? Andr...
    lucas777閱讀 5,202評論 2 54
  • 內(nèi)容 抽屜菜單 ListView WebView SwitchButton 按鈕 點贊按鈕 進度條 TabLayo...
    小狼W閱讀 1,613評論 0 10
  • "這個世界子房,真的有人過著你想過的生活" 這是從微博上看到的一句話证杭,下面附了9張圖片妒御,確實很好。 大部分人送讲,都會偶爾...
    上官雨菁ym閱讀 702評論 0 0
  • 在這個很難有隱私的網(wǎng)絡(luò)時代李茫,可能只有記憶是私密的。每個人都有屬于自己的記憶秸侣,可有時候你記憶深刻的人宠互,于那個人...
    綠窗子閱讀 176評論 0 0