Android 動畫實戰-仿微博雷達功能

前言

在應用中使用動畫,能夠給用戶帶來良好的交互體驗。經過以前對Android動畫的分類總結,嘗試了使用屬性動畫實現支付寶支付效果及購物車添加動畫的效果,今天在這裏模仿一下微博雷達頁面效果java

對Android動畫不太熟悉或遺忘的知識,能夠經過下面兩篇文章瞭解。git

Android 動畫總結Android 動畫實戰github

這次模仿新浪微博雷達頁的功能,雖然只有一個Activity,但使用到了不少知識。包括canvas

  • 屬性動畫(雷達效果圖)
  • Android touch 事件傳遞機制
  • Android 6.0 動態權限判斷
  • 百度LBS/POI 搜索
  • EventBus

有興趣的同窗能夠查看Github 源碼框架

效果圖

老習慣,先看看效果圖。ide

至於真實的微博雷達效果是怎樣,玩微博的同窗能夠對比一下。函數

功能分析

這裏主要從實現的幾個功能點作一下分析。post

雷達效果圖

總的來講,這個雷達效果圖應該是整個微博雷達頁面模仿效果類似度最高的一個View。使用屬性動畫實現這個雷達掃描效果很是簡單。學習

動畫初始化動畫

private void initRoateAnimator() {
        mRotateAnimator.setFloatValues(0, 360);
        mRotateAnimator.setDuration(1000);
        mRotateAnimator.setRepeatCount(-1);
        mRotateAnimator.setInterpolator(new LinearInterpolator());
        mRotateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mRotateDegree = (Float) animation.getAnimatedValue();
                invalidateView();
            }
        });
        mRotateAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                mTipText = "正在探索周邊的...";
                //旋轉動畫啓動後啓動掃描波紋動畫
                mOutGrayAnimator.start();
                mInnerWhiteAnimator.start();
                mBlackAnimator.start();
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                //取消掃描波紋動畫
                mOutGrayAnimator.cancel();
                mInnerWhiteAnimator.cancel();
                mBlackAnimator.cancel();
                //重置界面要素
                mOutGrayRadius = 0;
                mInnerWhiteRadius = 0;
                mBlackRadius = 0;
                mTipText = "未能探索到周邊的...,請稍後再試";
                invalidateView();
            }
        });
    }

    private void initOutGrayAnimator() {
        mOutGrayAnimator.setFloatValues(mBlackRadius, getMeasuredWidth() / 2);
        mOutGrayAnimator.setDuration(1000);
        mOutGrayAnimator.setRepeatCount(-1);
        mOutGrayAnimator.setInterpolator(new LinearInterpolator());
        mOutGrayAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mOutGrayRadius = (Float) animation.getAnimatedValue();
            }
        });
    }

    private void initInnerWhiteAnimator() {
        mInnerWhiteAnimator.setFloatValues(0, getMeasuredWidth() / 3);
        mInnerWhiteAnimator.setDuration(1000);
        mInnerWhiteAnimator.setRepeatCount(-1);
        mInnerWhiteAnimator.setInterpolator(new AccelerateInterpolator());
        mInnerWhiteAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mInnerWhiteRadius = (Float) animation.getAnimatedValue();
            }
        });
    }

    private void initBlackAnimator() {
        mBlackAnimator.setFloatValues(0, getMeasuredWidth() / 3);
        mBlackAnimator.setDuration(1000);
        mBlackAnimator.setRepeatCount(-1);
        mBlackAnimator.setInterpolator(new DecelerateInterpolator());
        mBlackAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mBlackRadius = (Float) animation.getAnimatedValue();
            }
        });
    }複製代碼

這裏首先定義了一些動畫效果,並在他們各自的Update 回調方法裏實現了屬性值的更新。這裏只有在mRotateAnimator的Update回調了執行了invalidateView(),避免了過渡繪製,浪費資源;屬性值每次更新後,就會調用onDraw 方法,會經過canvas繪製視圖,這樣不斷刷新,就會呈現出雷達掃描的效果。

canvas 繪製動畫

@Override
    protected void onDraw(Canvas canvas) {
        //繪製波紋
        canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, mBlackRadius, mBlackPaint);
        canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, mInnerWhiteRadius, mInnerWhitePaint);
        canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, mOutGrayRadius, mOutGrayPaint);

        //繪製背景
        Bitmap mScanBgBitmap = getScanBackgroundBitmap();
        if (mScanBgBitmap != null) {
            canvas.drawBitmap(mScanBgBitmap, getMeasuredWidth() / 2 - mScanBgBitmap.getWidth() / 2, getMeasuredHeight() / 2 - mScanBgBitmap.getHeight() / 2, new Paint(Paint
                    .ANTI_ALIAS_FLAG));
        }

        //繪製按鈕背景
        Bitmap mButtonBgBitmap = getButtonBackgroundBitmap();
        canvas.drawBitmap(mButtonBgBitmap, getMeasuredWidth() / 2 - mButtonBgBitmap.getWidth() / 2, getMeasuredHeight() / 2 - mButtonBgBitmap.getHeight() / 2, new Paint(Paint.ANTI_ALIAS_FLAG));

        //繪製掃描圖片
        Bitmap mScanBitmap = getScanBitmap();
        canvas.drawBitmap(mScanBitmap, getMeasuredWidth() / 2 - mScanBitmap.getWidth() / 2, getMeasuredHeight() / 2 - mScanBitmap.getHeight() / 2, new Paint(Paint.ANTI_ALIAS_FLAG));
        //繪製文本提示
        mTextPaint.getTextBounds(mTipText, 0, mTipText.length(), mTextBound);
        canvas.drawText(mTipText, getMeasuredWidth() / 2 - mTextBound.width() / 2, getMeasuredHeight() / 2 + mScanBackgroundBitmap.getHeight() / 2 + mTextBound.height() + 50, mTextPaint);

    }複製代碼

滑動推薦或不喜歡

這裏上拉推薦,下拉不感興趣的滑動效果和真實效果有必定差距。實現方案是借鑑下拉刷新和下拉加載框架的內容。只是修改了頭部和底部的隱藏View。同時,也須要實如今滑動時,對頭部和底部tab的隱藏效果。所以在touch事件的ACTION_DOWN 和ACTION_UP 環節,添加了回調單獨處理。

監聽滑動狀態

/** * 監聽當前是否處於滑動狀態 */
    public interface OnPullListener {
        /** * 手指正在屏幕上滑動 */
        void pull();

        /** * 手指已從屏幕離開,結束滑動 */
        void pullDone();
    }複製代碼

處理滑動

public boolean onTouchEvent(MotionEvent event) {

        int y = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // onInterceptTouchEvent已經記錄
                // mLastMotionY = y;
                break;
            case MotionEvent.ACTION_MOVE:

                if (mPullListener != null) {
                    mPullListener.pull();
                }

                int deltaY = y - mLastMotionY;
                if (mPullState == PULL_DOWN_STATE) {
                    // PullToRefreshView執行下拉
                    Log.i(TAG, " pull down!parent view move!");
                    headerPrepareToRefresh(deltaY);
                    // setHeaderPadding(-mHeaderViewHeight);
                } else if (mPullState == PULL_UP_STATE) {
                    // PullToRefreshView執行上拉
                    Log.i(TAG, "pull up!parent view move!");
                    footerPrepareToRefresh(deltaY);
                }
                mLastMotionY = y;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:


                int topMargin = getHeaderTopMargin();
                if (mPullState == PULL_DOWN_STATE) {
                    if (topMargin >= 0) {
                        // 開始刷新
                        headerRefreshing();
                    } else {
                        // 尚未執行刷新,從新隱藏
                        setHeaderTopMargin(-mHeaderViewHeight);
                        setHeadViewAlpha(0);
                        if (mPullListener != null) {
                            mPullListener.pullDone();
                        }
                    }
                } else if (mPullState == PULL_UP_STATE) {
                    if (Math.abs(topMargin) >= mHeaderViewHeight
                            + mFooterViewHeight) {
                        // 開始執行footer 刷新
                        footerRefreshing();
                    } else {
                        // 尚未執行刷新,從新隱藏
                        setHeaderTopMargin(-mHeaderViewHeight);
                        setFootViewAlpha(0);
                        if (mPullListener != null) {
                            mPullListener.pullDone();
                        }
                    }
                }
                break;
        }
        return super.onTouchEvent(event);
    }複製代碼

處理卡片切換

class MyHeadListener implements SmartPullView.OnHeaderRefreshListener {

        @Override
        public void onHeaderRefresh(SmartPullView view) {
            refreshView.onHeaderRefreshComplete();
            index = index + 1;
            cardAnimActions();
        }


    }
class MyFooterListener implements SmartPullView.OnFooterRefreshListener {

        @Override
        public void onFooterRefresh(SmartPullView view) {
            refreshView.onFooterRefreshComplete();
            index = index + 1;
            cardAnimActions();
        }
    }複製代碼

這裏咱們在上下拉刷新的執行回調中,當即完成相應的刷新流程,並執行一張卡片隱藏和下一張卡片顯示的動畫,這樣不管是上拉推薦仍是下拉不感興趣,都會去更新一次卡片內容。

卡片顯示隱藏動畫

private void cardAnimActions() {

        cardHideAnim.start();
        cardHideAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                Log.e(TAG, "onAnimationEnd: the index is " + index);
                backFrame.setBackgroundColor(colors[index % 3]);
                if (poiInfos != null && poiInfos.size() > 0) {
                    if (index < poiInfos.size()) {
                        name.setText(poiInfos.get(index).name);
                        address.setText(poiInfos.get(index).address);
                        phoneNum.setText(poiInfos.get(index).phoneNum);
                    }
                }
                cardShowAnim.start();
            }
        });

    }複製代碼

這裏cardHideAnim和cardShowAnim分別是兩個屬性 動畫的組合,兩者內容恰好相反,使用了卡片Scale和alpha的屬性動畫的組合;具體可查看源碼。

LBS定位和POI 搜索

經過上面的內容,完成了全部動畫相關的操做。接下來就是展現內容的實現了。

這裏的展現內容是根據當前位置的經緯度座標,按關鍵字去搜索周邊的興趣點,而關鍵字就是底部幾個tab所標示的內容。點擊底部tab便可以實現關鍵字的更新,從新發起搜索請求,實現UI更新。

這個過程分爲兩步,首先是進行定位(這裏固然首先要確保獲取到定位權限),獲取到當前位置;而後根據當前位置和關鍵字進行POI搜索,將搜索結果呈現出來便可。

關於如何使用百度地圖SDK配置AndroidManifest文件,申請key等相關操做,這裏再也不贅述,具體細節可參考官網

定位實現

首先須要進行定位以前的一些配置

mLocationClient = new LocationClient(getApplicationContext());     //聲明LocationClient類
        mLocationClient.registerLocationListener(this);    //註冊監聽函數
        LocationClientOption option = new LocationClientOption();
        option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy
        );//可選,默認高精度,設置定位模式,高精度,低功耗,僅設備
        option.setCoorType("bd09ll");//可選,默認gcj02,設置返回的定位結果座標系
        int span = 1000;
        option.setScanSpan(span);//可選,默認0,即僅定位一次,設置發起定位請求的間隔須要大於等於1000ms纔是有效的
           .....        (跟多配置信息可參考官網)
       mLocationClient.setLocOption(option);複製代碼

配置完成後,就能夠開始定位操做了,固然不能忘了申請權限

if (ContextCompat.checkSelfPermission(mContext,
                Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            //沒有定位權限則請求
            ActivityCompat.requestPermissions(this, permissons, MY_PERMISSIONS_REQUEST_LOCATION);

        } else {
            mLocationClient.start();
        }複製代碼

這樣,就會開始調用手機的定位功能開始定位,定位成功後,會執行onReceiveLocation回調方法,在這個方法裏能夠獲取到定位後的詳細信息。

@Override
    public void onReceiveLocation(BDLocation bdLocation) {
        if (mLocationClient != null && mLocationClient.isStarted()) {
            mLocationClient.stop();
        }

        district.setText(bdLocation.getAddress().district);
        latLng = new LatLng(bdLocation.getLatitude(), bdLocation.getLongitude());
        movie.performClick();
    }複製代碼

這個方法回調成功後,應該及時關閉定位操做;這裏咱們只是簡單的獲取了當前的區域位置,並設置在了頂部,同時得到了當前的經緯度信息。以後經過movie.performClick便開始了POI搜索的內容。

POI搜索實現

和定位功能相似,POI搜索功能開始以前,也須要進行相應的配置

mPoiSearch = PoiSearch.newInstance();
        mPoiSearch.setOnGetPoiSearchResultListener(new MyPoiSearchListener());
        mNearbySearchOption = new PoiNearbySearchOption()
                .radius(5000)
                .pageNum(1)
                .pageCapacity(20)
                .sortType(PoiSortType.distance_from_near_to_far);複製代碼

接着咱們就會按照剛纔的movie.performClick 方法,開始執行POI 搜索功能。

if (latLng != null && mNearbySearchOption != null && keyWord != null) {
            mNearbySearchOption.location(latLng).keyword(keyWord);
            mPoiSearch.searchNearby(mNearbySearchOption);
        }複製代碼

這裏將剛纔獲取到的Latlng 位置信息和keyword關鍵字信息注入到NearbySearchOption(POI 搜索中,附近位置搜索的配置對象)中,並使用這個NearbySearchOption開始POI搜索。一樣,在POI搜索完成後執行一個回調方法,在回調方法裏咱們能夠獲取到POI的搜索結果。

@Override
    public void onGetPoiResult(PoiResult poiResult) {
        Log.e("onGetPoiResult", "the poiResult " + poiResult.describeContents());
        EventBus.getDefault().post(poiResult);
    }複製代碼

顧名思義,返回的參數poiResult 就是POI搜索結果。這裏爲了減小Activity中代碼量,使用EventBus將搜索發送到了Activity中相應的Subscribe方法中。

@Subscribe
    public void onPoiResultEvent(PoiResult poiResult) {

        if (poiResult != null && poiResult.getAllPoi() != null && poiResult.getAllPoi().size() > 0) {
            poiInfos = poiResult.getAllPoi();
            name.setText(poiInfos.get(0).name);
            address.setText(poiInfos.get(0).address);
            phoneNum.setText(poiInfos.get(0).phoneNum);

            index = 1;

            if (refreshView.getVisibility() == View.GONE) {
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        radar.stopAnim();
                        radar.setVisibility(View.GONE);
                        refreshView.setVisibility(View.VISIBLE);
                        cardShowAnim.start();
                    }
                }, 3000);
            }
        } else {
            radar.stopAnim();
        }


    }複製代碼

這裏,根據搜索結果再次實現最終的UI更新。

到這裏,就完成了全部功能。

總結

關於這個微博雷達效果的模仿,從最開始只是模仿雷達掃描效果,最終到總體效果的實現。嘗試了不一樣的方案;不得不認可模仿效果和實際功能差不少。但也算是一個學習的過程當中,也踩到了一些一些沒注意的坑,也算是有點收穫吧。


最後,再次給出源碼地址Github,歡迎star & fork。

相關文章
相關標籤/搜索