Android animation combat - imitating Weibo radar function

foreword

Using animations in applications can bring a good interactive experience to users. Through the previous classification and summary of Android animation, I tried to use attribute animation to realize Alipay payment effect and shopping cart adding animation effect. Today, I will imitate the effect of Weibo radar page .

The knowledge that is not familiar with or forgotten about Android animation can be learned through the following two articles.

Android animation summary , Android animation combat

This time imitating the function of the Sina Weibo radar page, although there is only one Activity, but a lot of knowledge is used. Include
- Attribute animation (radar rendering)
- Android touch event delivery mechanism
- Android 6.0 dynamic permission judgment
- Baidu LBS/POI search
- EventBus

Interested students can view the Github source code .

renderings

Old habits, look at the renderings first.

Since the effect of using the simulator to intercept the gif is really terrible, so I can only put a static preview image. Interested friends, you can download APK_DEMO and install it on your phone to check the effect.
As for the real effect of Weibo radar, students who play Weibo can compare.

Functional Analysis

Here mainly from the realization of several functional points to do an analysis.

Radar renderings

In general, this radar rendering should be the View with the highest imitation effect on the entire Weibo radar page. Achieving this radar scan effect using property animation is very simple.

animation initialization

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();
            }
        });
    }

Here, some animation effects are first defined, and the update of property values ​​is implemented in their respective Update callback methods . Here, invalidateView() is executed only when the Update of mRotateAnimator calls back, which avoids transition drawing and wastes resources; after each update of the property value, the onDraw method will be called, and the view will be drawn through the canvas, so that the radar will be displayed if it is continuously refreshed. Scanning effect.

Canvas drawing animation

@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);

    }

Swipe to recommend or dislike

Pull-up is recommended here, and there is a certain gap between the sliding effect that is not interesting and the real effect. The implementation scheme is to draw on the content of the pull-down refresh and pull-down loading frameworks. Just modified the hidden View at the head and bottom. At the same time, it is also necessary to realize the hiding effect of the header and bottom tabs when sliding. Therefore, in the ACTION_DOWN and ACTION_UP links of the touch event, callbacks are added for separate processing.

monitor sliding state

   /**
     * 监听当前是否处于滑动状态
     */
    public interface OnPullListener {
    
    
        /**
         * 手指正在屏幕上滑动
         */
        void pull();

        /**
         * 手指已从屏幕离开,结束滑动
          */
        void pullDone();
    }

handle swipe

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);
    }

* handle card switching*

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();
        }
    }

Here we immediately complete the corresponding refresh process in the execution callback of the up and down refresh, and execute the animation of one card being hidden and the next card being displayed, so that whether it is a pull-up recommendation or a pull-down not interested, the card content will be updated once .

Card show hide animation


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();
            }
        });

    }

Here cardHideAnim and cardShowAnim are a combination of two attribute animations, the content of which is just the opposite, using a combination of card Scale and alpha attribute animations; see the source code for details.

LBS location and POI search

Through the above content, all animation-related operations are completed. Next is the realization of the display content.

The content displayed here is to search for nearby points of interest based on the latitude and longitude coordinates of the current location, and the keyword is the content indicated by the few tabs at the bottom. Click the bottom tab to update the keyword, re-initiate the search request, and update the UI.

This process is divided into two steps, the first is to locate (here, of course, you must first ensure that you have obtained the positioning permission), and then to obtain the current location; then to perform POI search based on the current location and keywords, and to present the search results.

About how to use Baidu Map SDK to configure AndroidManifest file, apply for key and other related operations, I will not repeat them here. For details, please refer to the official website

Positioning realization

First, some configuration before positioning is required


       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);

After the configuration is complete, you can start the positioning operation. Of course, you can't forget to apply for permissions.

if (ContextCompat.checkSelfPermission(mContext,
                Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            //没有定位权限则请求
            ActivityCompat.requestPermissions(this, permissons, MY_PERMISSIONS_REQUEST_LOCATION);

        } else {
            mLocationClient.start();
        }

In this way, the positioning function of the mobile phone will be called to start positioning. After the positioning is successful, the onReceiveLocation callback method will be executed. In this method, the detailed information after positioning can be obtained.

@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();
    }

After the callback of this method is successful, the positioning operation should be closed in time; here we simply obtain the current area position, set it at the top, and obtain the current latitude and longitude information. After that, the content of POI search is started through movie.performClick.

POI search implementation

Similar to the positioning function, the corresponding configuration is also required before the POI search function starts.

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

Then we will start the POI search function according to the movie.performClick method just now.

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

Here, the Latlng location information and keyword keyword information obtained just now are injected into NearbySearchOption (POI search, the configuration object of nearby location search), and this NearbySearchOption is used to start POI search. Similarly, a callback method is executed after the POI search is completed. In the callback method, we can get the search results of the POI.

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

As the name implies, the returned parameter poiResult is the POI search result. Here, in order to reduce the amount of code in the Activity, EventBus is used to send the search to the corresponding Subscribe method in the Activity.

@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();
        }


    }

Here again, the final UI update is implemented based on the search results.

At this point, all functions are completed.

Summarize

Regarding the imitation of this Weibo radar effect, from the very beginning, it was only to imitate the radar scanning effect, and finally to the realization of the overall effect. Tried different solutions; have to admit that the imitation effect is much worse than the actual function. But it is also a learning process, and I also stepped on some pits that I did not pay attention to, which can be regarded as a bit of gain.


Finally, give the source address Github again , welcome to star & fork.

Guess you like

Origin blog.csdn.net/TOYOTA11/article/details/54095456