使用MotionLayout實現高德地圖bottomSheets效果

高德效果

高德效果

以下是我用motionlayout實現的效果,沒有達到絲滑流暢,優化就看小伙伴你了

image

demo.apk下載體驗

緣由

  • 使用高德地圖的時候看著這種體驗很好,隨后就想試試怎么達到類似效果
  • 最近正在看MotionLayout的東西,正好就嘗試嘗試

MotionLayout

  • 「譯」 MotionLayout 介紹 (Part I - IV)系列教會你如何使用MotionLayout
  • 這里不做過多描述,總結一下在xml文件夾下創建xxscene.xml 主要用于描述場景動畫的關鍵幀和view狀態變化等
  • xxscene.xml內容包括 主要為3個關鍵內容:
  1. Transition 過渡

constraintSetStart:啟動約束場景

constraintSetEnd:結束約束場景

app:dragDirection="dragUp" 拽動(拖拉)

  1. KeyFrameSet關鍵幀集合

KeyAttribute關鍵幀

app:framePosition 位置,進度

app:target="@id/xxx 被描述的view id

  1. ConstraintSet 約束集合
 <Transition
        app:constraintSetEnd="@id/slideup_end"
        app:constraintSetStart="@id/slideup_start"
        app:duration="600"
        app:interpolator="easeIn">
        <OnSwipe
            app:dragDirection="dragUp"
            app:maxAcceleration="600"
            app:touchAnchorSide="top"
            app:touchAnchorId="@id/content"
           />
        <KeyFrameSet>
            <KeyAttribute
                android:alpha="0"
                app:framePosition="45"
                app:target="@id/sugar_title" />

            <KeyAttribute
                android:alpha="1"
                app:framePosition="90"
                app:target="@id/sugar_title" />
        ...
        </KeyFrameSet>
 </Transition>   
 
 <ConstraintSet android:id="@+id/slideup_start">

        <Constraint
        ···
        />
    ...
  </ConstraintSet>  

拆解過程

  • 高德地圖是上拉之后是三段式的,如圖所示
1
2
3
  • MotionLayout就只有一個初始約束和結束約束,沒有中間約束,如何實現這種三段式效果?
  • 答: 使用progress, MotionLayout自帶進度
  • 有進度,什么時候執行下一步操作,什么時候又執行上一步操作?
  • 答: 根據手勢,我們可以判斷用戶下一步是往上拉還是下拉,設定階段閥值,超過進入下一步,未超過回到之前一步
  • 高德地圖中,只有手觸碰到bottomview的時候手勢才有效果,所以還需要判斷touch事件是否在view范圍內

拆解完畢

實現過程

  • 設置閥值
    /**
     * 初始位置
     */
    public final static float PROGRESS_START = 0f;
    /**
     * 頂部閥值 
     */
    public final static float PROGRESS_TOP = 0.9f;
    /**
     * 低部閥值 
     */
    public final static float PROGRESS_BOTTOM = 0.1f;
    /**
     * 中間位置 
     */
    public final static float PROGRESS_MIDDLE = 0.6f;
    /**
     * 結束位置 
     */
    public final static float PROGRESS_END = 1.0f;
  • 重寫MotionLayoutonTouchEvent事件 ,使用hasMiddle布爾值判斷是否有中間狀態
 @Override
    public boolean onTouchEvent(MotionEvent event) {
        float progress = getProgress();
        View viewGroup = findViewById(R.id.content);
        Rect mRect = new Rect();
        if (!mTouchStared) {
            viewGroup.getHitRect(mRect);
            mTouchStared = mRect.contains((int) event.getX(), (int) event.getY());
        }
        float endY;
        if (hasMiddle) {
            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_CANCEL:
                    mTouchStared = false;
                    break;
                case MotionEvent.ACTION_DOWN:
                    startY = event.getY();
                    break;
                case MotionEvent.ACTION_UP:
                    endY = event.getY();
                    //手勢向下
                    if ((endY - startY) > 0) {
                        if (progress >= PROGRESS_TOP) {
                            mTouchStared = false;
                            handleProgress(PROGRESS_END);

                        }
                        if (progress < PROGRESS_TOP && progress >= PROGRESS_MIDDLE) {
                            handleProgress(PROGRESS_MIDDLE);
                        }
                        if (progress < PROGRESS_MIDDLE) {
                            handleProgress(PROGRESS_START);
                        }
                        //手勢向上
                    } else {
                        if (progress <= PROGRESS_BOTTOM) {
                            handleProgress(PROGRESS_START);
                        }
                        if (progress > PROGRESS_BOTTOM && progress <= PROGRESS_MIDDLE) {
                            handleProgress(PROGRESS_MIDDLE);
                        }
                        if (progress > PROGRESS_MIDDLE) {
                            mTouchStared = false;
                            handleProgress(PROGRESS_END);
                        }
                    }
                    return mTouchStared;
            }
        } else {
            if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || event.getActionMasked() == MotionEvent.ACTION_UP) {
                mTouchStared = false;
                return super.onTouchEvent(event);
            }
        }
        return mTouchStared && super.onTouchEvent(event);

    }
  • bottom_scene.xml
  1. 上拉超過頂部閥值PROGRESS_TOP之后標題出現在屏幕內,其余時候出現在屏幕外即可;
  2. 初始狀態這里把scaleXscaleY設為0.9結束設為了1,僅僅是為了過渡好看,你可以不用設置隨意修改即可
  3. 背景色過渡,最開始透明,結束為白色背景。中間過渡關鍵幀95變純白背景

結果和改進

  • 設置允許中間狀態后,之后進入下一步的過程,如圖,過于生硬
image
  • 改進方向
  1. 動畫應該是勻速的,然而setProgress(pro);卻是一步直達;
  2. 設置時間間隔勻速達到最后的進度即可,源碼已詳細注釋。改進之后見最上面效果圖;
 private void handleProgress(float progress) {
        //如果需要設置的進度和當前進度相同不做處理
        if (progress == getProgress()){
            return;
        }
        //動畫播放時間底值
        long time = 200;
        //進度間隔 >0 說明上拉 < 0說明下滑
        float interval = progress - getProgress();
        long startTime, endTime;
        if (interval > 0) {
            startTime = (long) (getProgress() * time);
            endTime = (long) (progress * time);
        } else {
            endTime = (long) (getProgress() * time);
            startTime = (long) (progress * time);
        }
        if (timeDisposable != null){
            timeDisposable.dispose();
        }
        //startTime 初始時間 endTime - startTime為次數 0為延遲時間 3為間隔 單位TimeUnit.MILLISECONDS 毫秒
        timeDisposable = Observable.intervalRange(startTime, endTime - startTime, 0, 3, TimeUnit.MILLISECONDS)
                .observeOn(Schedulers.io())
                .compose(((BaseActivity) getContext()).getProvider().bindToLifecycle())
                .observeOn(AndroidSchedulers.mainThread())
                .map(new Function<Long, Long>() {
                    @Override
                    public Long apply(Long aLong) throws Exception {
                        //下滑需要反向
                        if (interval < 0) {
                            long interStart = aLong - startTime;
                            return endTime - interStart;
                        }
                        return aLong;
                    }
                })
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(Long aLong) throws Exception {
                        float pro = (Float.valueOf(aLong) / time);
                        setProgress(pro);
                    }
                });
    }

源碼已放入sugar demo中,sugar是我會長期維護的一個庫??????

?? Sugar 簡單便捷 快速開發Android項目,集合流行框架封裝

About me

License

Copyright 2019, wobiancao       
  
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at 
 
       http://www.apache.org/licenses/LICENSE-2.0 

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
   
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容