Project address: https://github.com/razerdp/FriendCircle lu a circle of friends together this is this article locates the corpus, all updates will be in this anthology oh, welcome the attention

The next link: http://www.jianshu.com/p/94e1e267b3b3

Is the title very simple and rude ←_←

Well, let’s get down to business. GitHub has written all about this project, so let’s cut to the chase and get straight to the point.

Wechat circle of friends in the version I know, there are two (nonsense ORz), one is IOS, one is Android, (nonsense again).

IOS because of the unique UI implementation advantages, can easily make a variety of pleasing to the eye and very forced case of animation, which can be bitter Android, in contrast, Android in order to achieve a few animation will have to write more than N lines of code, such as the drop-down refresh of moments of friends.

There is an obvious difference between the two systems in the drop-down refresh of the moments of friends, which lies in the refreshed icon. In Android, the refreshed icon is always in the headerView, and at the bottom of the HeaderView, unable to break through the limitation of headerView, while in ios version, Icon is not controlled by the ListView, and the two seem to be separate. So in ios, refreshed ICONS can be pulled down as the ListView is pulled down.

The above mentioned may be a little unclear, you can find two systems of mobile phone brush once, pay attention to refresh Icon action, you will know what happened.

So, as a high-powered Android app, of course we have to challenge the ios refresh.

Hence, the first in our series.

Without further ado, preview images are sent :(please ignore qiong mei)

Library selection completed, the next is to think.

First of all, our refresh icon must break the listView limit, so this icon must not be a part of the ListView, so I temporarily think of the following two schemes:

  • The icon uses imageView and is present in the layout file alone rather than as part of the ListView
  • Icon uses imageView and adds one dynamically using WindowManager

For convenience, I adopted the first option. Here is our layout file: I know that copying the XML code directly is long and smelly, so I took a screenshot at the bottom:

As you can see, our layout is pretty neat, top to bottom is ListView -> ImageView -> ActionBar, why DO I put it that way, it’s all about the order in which the layout file is drawn,

Drawing starts at the root of the layout. The layout hierarchy is drawn in declared order. For example, a parent view is drawn before its children, who are drawn in the declared order.

To put it simply, visually, it is to draw the top first, then draw the bottom.

So our layout looks like this:

  • First draw a listview
  • Draw our icon (above the Listviews)
  • Finally draw the Actionbar (so that it can cover the Icon and ListView)

At this point, we have a rough idea of the implementation:

  • When the ListView is pulled down, the distance callback controls the distance between our icon and the top (topMargin), and the ListView is pulled down without interfering with each other
  • When pulled to the refresh distance, let go, listView will bounce back, icon because of the set margin, so will keep the refresh distance position, at this time play animation (continuous rotation), and refresh operation
  • After the refresh, since our ListView has bounced back and no displacement information is available at this point, we need to use a thread to manually create an interpolator to dynamically update the icon’s margin back to the top and hide it under the Actionbar.

The scheme above looks complicated, and it is, but fortunately the drop-down framework already implements the most troublesome interfaces, saving us at least 70% of the time thanks to the PtrUIHandler and PtrHandler callbacks.

Next, let’s implement the header preliminarily. Our header has no function. It only has one function, that is, the color of the Overscroll after the drop-down, so its layout is also very simple:

We tentatively defined the height as 300dp, because in my tests, even though I pulled from the top to the bottom, our header still didn’t show up completely (thanks to the damping parameters), so 300DP was enough

After the layout is complete, we whip out our code:

public class FriendCirclePtrHeader extends RelativeLayout {
    private static final String TAG = "FriendCirclePtrHeader";

    private ImageView mRotateIcon;
    private View rootView;
    private boolean isAutoRefresh;
    private RotateAnimation rotateAnimation;
    private SmoothChangeThread mSmoothChangeThread;

    // Current status
    private PullStatus mPullStatus;

    public FriendCirclePtrHeader(Context context) {
        this(context, null);
    }

    public FriendCirclePtrHeader(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FriendCirclePtrHeader(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public FriendCirclePtrHeader(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView(context);
    }

    private void initView(Context context) {
        rootView = LayoutInflater.from(context).inflate(R.layout.widget_ptr_header, this.false);
        addView(rootView);

        rotateAnimation = new RotateAnimation(0.360, Animation.RELATIVE_TO_SELF, 0.5 f, Animation.RELATIVE_TO_SELF,
                0.5 f);
        rotateAnimation.setDuration(600);
        rotateAnimation.setInterpolator(new LinearInterpolator());
        rotateAnimation.setRepeatCount(Animation.INFINITE);
    }
Copy the code

We inflate a view directly out and add it to our header, initializing some anima

Now comes the main implementation:

 //=============================================================ptr:
    private PtrUIHandler mPtrUIHandler = new PtrUIHandler() {
        /** returns to the initial position */
        @Override
        public void onUIReset(PtrFrameLayout frame) {
            mPullStatus = PullStatus.NORMAL;
            if(mRotateIcon.getAnimation() ! =null) { mRotateIcon.clearAnimation(); }}/** leave the initial position */
        @Override
        public void onUIRefreshPrepare(PtrFrameLayout frame) {}/** Start to refresh animation */
        @Override
        public void onUIRefreshBegin(PtrFrameLayout frame) {
            mPullStatus = PullStatus.REFRESHING;
            if(mRotateIcon ! =null) {
                if(mRotateIcon.getAnimation() ! =null) { mRotateIcon.clearAnimation(); } mRotateIcon.startAnimation(rotateAnimation); }}/** Refresh complete */
        @Override
        public void onUIRefreshComplete(PtrFrameLayout frame) {
            mPullStatus = PullStatus.NORMAL;
            if (mSmoothChangeThread==null){
                mSmoothChangeThread=SmoothChangeThread.CreateLinearInterpolator(mRotateIcon,frame.getOffsetToRefresh
                        (),0.300.75);
                mSmoothChangeThread.setOnSmoothResultChangeListener(new SmoothChangeThread.OnSmoothResultChangeListener() {
                    @Override
                    public void onSmoothResultChange(int result) {
                        updateRotateAnima(result);
                        mRotateIcon.setRotation(-(result << 1)); }}); }else {
                mSmoothChangeThread.stop();
            }
            mRotateIcon.post(mSmoothChangeThread);

        }

        /** Shift update overload */
        @Override
        public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) {
            final int mOffsetToRefresh = frame.getOffsetToRefresh();
            final int currentPos = ptrIndicator.getCurrentPosY();
            final int lastPos = ptrIndicator.getLastPosY();

            if (currentPos < mOffsetToRefresh) {
                // The refresh line was not reached
                if(status == PtrFrameLayout.PTR_STATUS_PREPARE && mRotateIcon ! =null) {
                    updateRotateAnima(currentPos);
                    mRotateIcon.setRotation(-(currentPos << 1)); }}else if (currentPos > mOffsetToRefresh) {
                // Reach or exceed the refresh line
                if(isUnderTouch && status == PtrFrameLayout.PTR_STATUS_PREPARE && mRotateIcon ! =null) {
                    updateRotateAnima(mOffsetToRefresh);
                    mRotateIcon.setRotation(-(currentPos << 1)); }}}};private void updateRotateAnima(int marginTop) {
        Log.d(TAG, "curMargin=========" + marginTop);
        if (mRotateIcon == null) return;
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mRotateIcon.getLayoutParams();
        params.topMargin = marginTop;
        mRotateIcon.setLayoutParams(params);
    }
Copy the code

The PtruiHandler is the callback that the framework exposes to control UI dropdowns, and the information is noted in the comments.

Here we focus on this callback: The onUIRefreshComplete callback is executed when the external ptrFrame.refreshComplete () is done, but our ListView has bounced back, meaning there is no displacement information for us to update the topMargin. If there is no displacement, If we just updateRotateAnima(0), all we see on the screen is that our icon disappears without a transition animation, so we do this through a thread


/ * * *@descSmooth scrolling thread, used to recursively call itself to implement smooth scrolling for a view * */
public class SmoothChangeThread implements Runnable {
    // View to manipulate
    private View v = null;
    // The original Y coordinate
    private int fromY = 0;
    // Target Y coordinates
    private int toY = 0;
    // Animation execution time (ms)
    private long durtion = 0;
    / / frame rate
    private int fps = 60;
    // Interval (ms), interval = 1000 / frame rate
    private int interval = 0;
    // Start time. -1 indicates that the system is not started
    private long startTime = -1;
    // speed interpolator
    private static Interpolator mInterpolator = null;
    private OnSmoothResultChangeListener mListener;

    public static SmoothChangeThread CreateLinearInterpolator(View v, int fromY, int toY, long durtion, int fps){
        mInterpolator=new LinearInterpolator();
        return new SmoothChangeThread(v,fromY,toY,durtion,fps);
    }
    public static SmoothChangeThread CreateDecelerateInterpolator(View v, int fromY, int toY, long durtion, int fps){
        mInterpolator=new DecelerateInterpolator();
        return new SmoothChangeThread(v,fromY,toY,durtion,fps);
    }
    public static SmoothChangeThread CreateAccelerateDecelerateInterpolator(View v, int fromY, int toY, long durtion, int fps){
        mInterpolator=new AccelerateDecelerateInterpolator();
        return new SmoothChangeThread(v,fromY,toY,durtion,fps);
    }

    / * * * *@param v view
     * @paramFromY raw data *@paramToY Target data *@paramDurtion Duration *@paramFPS frames * /
    private SmoothChangeThread(View v, int fromY, int toY, long durtion, int fps) {
        this.v = v;
        this.fromY = fromY;
        this.toY = toY;
        this.durtion = durtion;
        this.fps = fps;
        this.interval = 1000 / this.fps;
    }

    @Override
    public void run(a) {
        // Check whether it is the first time to start. If it is the first time to start, record the timestamp of the start. This value is assigned only once
        if (startTime == -1) {
            startTime = System.currentTimeMillis();
        }
        // Get the timestamp of the current instant
        long currentTime = System.currentTimeMillis();
        // To enlarge the floating-point precision of division calculation
        int enlargement = 1000;
        // Calculate the percentage of the animation time that the current instant runs to
        float rate = (currentTime - startTime) * enlargement / durtion;
        // This ratio can not be between 0 and 1. When magnified, it is between 0 and 1000
        rate = Math.max(Math.min(rate, 1000),0);
        // Take the animation progress through the interpolator to get the response ratio, multiply by the start and target coordinates to get the current moment, the view should scroll distance.
        int changeDistance = Math.round((fromY - toY) * mInterpolator.getInterpolation(rate / enlargement));
        int currentY = fromY - changeDistance;
        if(mListener! =null){
            mListener.onSmoothResultChange(currentY);
        }

        if(currentY ! = toY) { v.postDelayed(this.this.interval);
        }
        else {
            return; }}public void stop(a) {
        v.removeCallbacks(this);
        startTime=-1;
    }

    public OnSmoothResultChangeListener getOnSmoothResultChangeListener(a) {
        return mListener;
    }

    public void setOnSmoothResultChangeListener(OnSmoothResultChangeListener listener) {
        mListener = listener;
    }

    public interface OnSmoothResultChangeListener{
        void onSmoothResultChange(int result); }}Copy the code

The Java source file is on the Internet looking for a custom interpolator, after I modified, throw out the result calculated by the callback interface to, and using the static factory provide different types of interpolation effect, we can through this interface dynamic update our margin (ps: this tool can also be used in many places?)

At this point, our header is basically customized, the complete code can be viewed on Github, the next step is to implement the encapsulation of ptrFrame, let it become our PtrListView.


Gorgeous dividing line


I’ve received some comments over the past few days, which go something like this:

  1. Why not recylerView
  2. Why not use ValueAnimator instead of threads

Here are the answers:

  1. Because at present to tell the truth, most projects have been using listView, and involved in a relatively deep, so here we use listView, secondly, actually I like recylerView said… Also, the framework supports adding any view, so you can change it to RecylerView if you like.
  2. I was trying to figure out how to update the margin, and I thought “Thread calculation stupid”, so I did it. After reading the comments… Why don’t I use ValueAnimator? I’m stupid!! Git is now updated. Two ways to do it -V-

The update code is as follows:

  /** Refresh complete */
        @Override
        public void onUIRefreshComplete(PtrFrameLayout frame) {
            mPullState = PullState.NORMAL;
            if (mRotateIcon==null)return;
            /** take the common interpolator thread implementation */
           /* if (mSmoothChangeThread == null) { mSmoothChangeThread = SmoothChangeThread.CreateLinearInterpolator(mRotateIcon, frame.getOffsetToRefresh(), 0, 300, 75); mSmoothChangeThread.setOnSmoothResultChangeListener( new SmoothChangeThread.OnSmoothResultChangeListener() { @Override public void onSmoothResultChange(int result) { updateRotateAnima(result); mRotateIcon.setRotation(-(result << 1)); }}); } else { mSmoothChangeThread.stop(); } mRotateIcon.post(mSmoothChangeThread); * /

            / * * take valueAnimator * /
            if (mValueAnimator==null){
                mValueAnimator=ValueAnimator.ofInt(frame.getOffsetToRefresh(),0);
                mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        int result= (int) animation.getAnimatedValue();
                        updateRotateAnima(result);
                        mRotateIcon.setRotation(-(result << 1)); }}); mValueAnimator.setDuration(300);
            }
            mValueAnimator.start();
        }
Copy the code

Both methods are retained