[Android] Screen lag, optimized list fluency, pull down to refresh, pull up to load more components, RefreshLayout modification

I have also written about similar components before:
Address: Pull down to refresh & pull up to load more components SmartRefreshLayout

I originally planned to replace it with this one, but after careful research I found it was not suitable. The functions are very good, but they cannot be embedded in the current engineering system. The reason is that everyone understands the system. The project configuration parameters that need to be changed for such components may be incompatible. So it can’t be used for the time being.
If it can be replaced with this one, there won’t be any component issues, probably like that.
Currently it is also an open source component
Let’s review list layout and logical processing:
xml layout

 <android.support.v4.widget.NestedScrollView
            android:id="@ + id/nestedScrollView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fillViewport="true"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <live.bingoogolapple.refreshlayout.BGARefreshLayout
                android:id="@ + id/mRefreshLayout"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:layout_behavior="@string/appbar_scrolling_view_behavior">

                <android.support.v7.widget.RecyclerView
                    android:id="@ + id/recycleView_playback"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:background="#FFFFFFFF"
                    android:nestedScrollingEnabled="false"
                    android:paddingLeft="20dp"
                    android:paddingRight="10dp"
                    android:paddingBottom="8dp"
                    app:layout_behavior="@string/appbar_scrolling_view_behavior" />
            </live.bingoogolapple.refreshlayout.BGARefreshLayout>
        </android.support.v4.widget.NestedScrollView>

java logic processing:

 BGARefreshViewHolder bgaNormalRefreshViewHolder = new BGANormalRefreshViewHolder(this, true);
        mRefreshLayout.setRefreshViewHolder(bgaNormalRefreshViewHolder);

        nestedScrollView = findViewById(R.id.nestedScrollView);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {<!-- -->
            nestedScrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() {<!-- -->
                @Override
                public void onScrollChange(View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {<!-- -->
                    NestedScrollView toNesstedScrollView = (NestedScrollView) view;
                   
                    if (scrollY < 5
                            || toNesstedScrollView.getChildAt(0).getMeasuredHeight()
                            == view.getMeasuredHeight()) {<!-- -->
                        return;
                    }
                    int height = toNesstedScrollView.getChildAt(0).getMeasuredHeight()
                            - view.getMeasuredHeight();
                    if (scrollY == height) {<!-- -->
                        //Set the proxy for BGARefreshLayout
                        if(mRefreshLayout.getDelegate()==null){<!-- -->
                             mRefreshLayout.setDelegate(XXXXActivity.this);
                        }
                        mRefreshLayout.beginLoadingMore();
                    }
                }
            });
        }

This layout is to achieve a head effect similar to the following picture:

This blog has introduced such a layout before: [Android] Folding effect CoordinatorLayout + AppBarLayout homepage effect & CoordinatorLayout jitter problem solution – 100 classic UI design templates (95/100)

The crux of the problem

NestedScrollView nested sub-component BGARefreshLayout; BGARefreshLayout nested sub-component: RecyclerView;
The sliding control listener is the OnScrollChangeListener in the NestedScrollView component:

nestedScrollView = findViewById(R.id.nestedScrollView);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {<!-- -->
            nestedScrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() {<!-- -->
                @Override
                public void onScrollChange(View view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {<!-- -->
                    NestedScrollView toNesstedScrollView = (NestedScrollView) view;
                  
                    if (scrollY < 5
                            || toNesstedScrollView.getChildAt(0).getMeasuredHeight()
                            == view.getMeasuredHeight()) {<!-- -->
                        return;
                    }
                    int height = toNesstedScrollView.getChildAt(0).getMeasuredHeight()
                            - view.getMeasuredHeight();
                    if (scrollY == height) {<!-- -->
                        //Set the proxy for BGARefreshLayout
                        if(mRefreshLayout.getDelegate()==null){<!-- -->
                             mRefreshLayout.setDelegate(XXXXActivity.this);
                            mRefreshLayout.setParentView(nestedScrollView);
                        }
                        mRefreshLayout.beginLoadingMore();
                    }
                }
            });
        }

Leading to more monitoring problems in pull-up refresh and pull-down loading.
The monitoring logic in BGARefreshLayout is as follows:

@Override
    protected void onAttachedToWindow() {<!-- -->
        super.onAttachedToWindow();

        // Set the listener after being added to the window, so developers don't have to worry about initializing RefreshLayout first or setting a custom scroll listener first.
        if (!mIsInitedContentViewScrollListener & amp; & amp; mLoadMoreFooterView != null) {<!-- -->
            setRecyclerViewOnScrollListener();
            setAbsListViewOnScrollListener();

            addView(mLoadMoreFooterView, getChildCount());

            mIsInitedContentViewScrollListener = true;
        }
    }

It is expected that the settings in setRecyclerViewOnScrollListener() will not take effect:

/**
     *
     * The specific effect depends on the xml layout structure
     * For example, the following does not apply:
     * NestedScrollView
     * BGARefreshLayout
     *RecyclerView
     *
     * */
    private void setRecyclerViewOnScrollListener() {<!-- -->
        if (mRecyclerView != null) {<!-- -->
            mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {<!-- -->
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {<!-- -->
                    if ((newState == RecyclerView.SCROLL_STATE_IDLE
                            || newState == RecyclerView.SCROLL_STATE_SETTLING)
                             & amp; & amp; shouldHandleRecyclerViewLoadingMore(mRecyclerView)) {<!-- -->
                        beginLoadingMore();
                        return;
                    }

                }

                /**
                 * dx: horizontal scroll distance
                 * dy: vertical scrolling distance
                 * */
                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {<!-- -->
                    super.onScrolled(recyclerView, dx, dy);
// Log.d(TAG, "dx = " + dx + " ,dy= " + dy);
                    if (dy < 0) {<!-- -->//When the finger scrolls down, the list scrolls to display the above content
                        mLoadMoreFooterView.setVisibility(GONE);
                    }
                }
            });
        }
    }

No matter who you ask to reason with, forget it if it doesn’t work. The problem is if you don’t pay attention. I can’t find such component problems at all.
The effects of OnScrollChangeListener and RecyclerView.OnScrollListener in the NestedScrollView component are consistent, both of which start loading more logic beginLoadingMore():

/**
     * Start pulling up to load more, which will trigger the delegate's onBGARefreshLayoutBeginRefreshing method
     */
    public void beginLoadingMore() {<!-- -->
        Log.d(TAG, "beginLoadingMore: called!");
        if (!mIsLoadingMore & amp; & amp; mLoadMoreFooterView != null
                 & amp; & amp; mDelegate != null
                 & amp; & amp; mDelegate.onBGARefreshLayoutBeginLoadingMore(this)) {<!-- -->
            mIsLoadingMore = true;
            Log.d(TAG, "run:mIsLoadingMore=" + mIsLoadingMore);
            if (mIsShowLoadingMoreView) {<!-- -->
                showLoadingMoreView();
            }
        }
    }

/**
     * Show pull-up to load more controls
     */
    public void showLoadingMoreView() {<!-- -->
        mRefreshViewHolder.changeToLoadingMore();
        mLoadMoreFooterView.setVisibility(VISIBLE);
        mLoadMoreFooterView.findViewById(R.id.layout_loading).setVisibility(VISIBLE);
        mLoadMoreFooterView.findViewById(R.id.tv_normal_refresh_footer_status_finish).setVisibility(GONE);

      BGARefreshScrollingUtil.scrollToBottom(mParentNestedScrollView);
        BGARefreshScrollingUtil.scrollToBottom(mScrollView);
        BGARefreshScrollingUtil.scrollToBottom(mRecyclerView);
        BGARefreshScrollingUtil.scrollToBottom(mAbsListView);
        if (mStickyNavLayout != null) {<!-- -->
            mStickyNavLayout.scrollToBottom();
        }
    }

BGARefreshScrollingUtil.scrollToBottom(mParentNestedScrollView) This is newly added after optimization;
If you don’t have this, you will just slide to the bottom and load more directly, and there will be no prompt to load more progress bars. You need to slide to the bottom of the list and then pull up again to get the prompt UI screen. This is too hidden, and it feels weird every time, but I just didn’t find anything wrong with this interactive screen. The main thing is that I don’t think about it in this regard. Because they all look natural and normal.

This code in showLoadingMoreView
BGARefreshScrollingUtil.scrollToBottom(mParentNestedScrollView) tells the list to display the prompt mLoadMoreFooterView when sliding to the bottom, and then automatically slide up to display LoadMoreFooterView on the screen.

BGARefreshScrollingUtil.scrollToBottom(mParentNestedScrollView) logic is as follows:

 /**
     * nestedScrollView scroll component sets scroll to the bottom
     * Use post (new runnable) method to scroll to the bottom after the scroll component is rendered.
     *
     * @param nestedScrollView scrolling component
     * */
    public static void scrollToBottom(final NestedScrollView nestedScrollView) {<!-- -->
        if (nestedScrollView != null) {<!-- -->
            nestedScrollView.post(new Runnable() {<!-- -->
                @Override
                public void run() {<!-- -->
                    nestedScrollView.fullScroll(ScrollView.FOCUS_DOWN);
                }
            });
        }
    }

Summary

It is because this sliding effect is incompatible that it leads to a series of magical situations:
1. When loading more pull-up interactions, you cannot see the loading more prompt UI because it does not automatically slide up to the end; so you often do not see it.

2. This will lead to confusion in the interval between api request calls for initiating a new page of data. Multiple calls may occur in a short period of time, and then a large amount of data will be loaded within this time period, causing the page to get stuck. The more stuck the page is, the more serious the problem caused by the continuity of initiated requests. Then the user will swipe more times. Trapped in an infinite loop. The detected continuous api data requests that are inconsistent with expectations initiated during a certain debugging are recorded as shown below.

3. Then the inevitable memory consumption increases sharply, and the memory pressure rises sharply in a short period of time (in fact, the final optimization has nothing to do with memory. After the optimization, it is just using clearMemory )

The above problems are caused by improper use of RefreshLayout. But it is almost impossible to find it without investing a lot of energy in carefully analyzing network data, memory, interactions, etc. Because the mobile phone screen behaved very much like a memory problem, so two or three days of fiddling with the memory did not alleviate the lag.

Solution

Finally, the optimized RefreshLayout logic processing is attached:


import live.bingoogolapple.refreshlayout.util.BGARefreshScrollingUtil;

/**
 * Author: Wang Hao Email: [email protected]
 * Creation time: 15/5/21 22:35
 * Description: Pull down to refresh, pull up to load more, and can add custom (fixed, sliding) head controls (such as the advertising space at the top of the MOOC app)
 */
public class BGARefreshLayout extends LinearLayout {
  
    /**
     * The currently adapted scrolling components are:
     *AbsListView
     *ScrollView
     * NestedScrollView
     *RecyclerView
     *
     * Automatically add a pop-up "Loading" progress bar prompt at the bottom when scrolling to the bottom
     * */
    private AbsListView mAbsListView;
    privateScrollView mScrollView;
    private NestedScrollView mParentNestedScrollView; // Outer parent component nested NestedScrollView
    private RecyclerView mRecyclerView;
    private View mNormalView;
    private WebView mWebView;
    private BGAStickyNavLayout mStickyNavLayout;
    private View mContentView;

    private float mInterceptTouchDownX = -1;
    private float mInterceptTouchDownY = -1;
    /**
     * The paddingTop of the entire head control when pressed
     */
    private int mWholeHeaderViewDownPaddingTop = 0;
    /**
     * Record downY when the pull-down refresh starts
     */
    private int mRefreshDownY = -1;

    /**
     * Whether the content control scroll listener has been set
     */
. . .

    /**
     *Constructed in xml layout
     * */
    public BGARefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOrientation(LinearLayout.VERTICAL);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mHandler = new Handler(Looper.getMainLooper());
        initWholeHeaderView();
    }

  . . .
    /**
     * {@link BGARefreshLayout} nests a parent scroll component adaptation in the outer layer
     * Currently adapted to NestedScrollView as the parent component
     *
     * */
    public void setParentView(View view) {

        if (null == view)
            return;
        /**
         * Layout nested as
         * NestedScrollView
         *BGARefreshLayout
         *RecyclerView
         * */
        if (view instanceof NestedScrollView) {
            mParentNestedScrollView = (NestedScrollView) view;
        }
    }

   
    /**
     * Initialize pull-up to load more controls
     *
     * @return
     */
    private void initLoadMoreFooterView() {
        mLoadMoreFooterView = mRefreshViewHolder.getLoadMoreFooterView();
        if (mLoadMoreFooterView != null) {
            // Measure the height of the pull-up to load more controls
            mLoadMoreFooterView.measure(0, 0);
            mLoadMoreFooterViewHeight = mLoadMoreFooterView.getMeasuredHeight();
            mLoadMoreFooterView.setVisibility(GONE);
            mLoadMoreFooterView.findViewById(R.id.layout_loading).setVisibility(GONE);
            mLoadMoreFooterView.findViewById(R.id.tv_normal_refresh_footer_status_finish).setVisibility(GONE);
        }
    }



    /**
     *
     * The specific effect depends on the xml layout structure
     * For example, the following does not apply:
     * NestedScrollView
     * BGARefreshLayout
     *RecyclerView
     *
     * */
    private void setRecyclerViewOnScrollListener() {<!-- -->
        if (mRecyclerView != null) {<!-- -->
            mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {<!-- -->
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {<!-- -->
                    if ((newState == RecyclerView.SCROLL_STATE_IDLE
                            || newState == RecyclerView.SCROLL_STATE_SETTLING)
                             & amp; & amp; shouldHandleRecyclerViewLoadingMore(mRecyclerView)) {<!-- -->
                        beginLoadingMore();
                        return;
                    }

                }

                /**
                 * dx: horizontal scroll distance
                 * dy: vertical scrolling distance
                 * */
                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {<!-- -->
                    super.onScrolled(recyclerView, dx, dy);
// Log.d(TAG, "dx = " + dx + " ,dy= " + dy);
                    if (dy < 0) {<!-- -->//When the finger scrolls down, the list scrolls to display the above content
                        mLoadMoreFooterView.setVisibility(GONE);
                    }
                }
            });
        }
    }

 

    /**
  
    /**
     * Start pulling up to load more, which will trigger the delegate's onBGARefreshLayoutBeginRefreshing method
     */
    public void beginLoadingMore() {
        Log.d(TAG, "beginLoadingMore: called!");
        if (!mIsLoadingMore & amp; & amp; mLoadMoreFooterView != null
                 & amp; & amp; mDelegate != null
                 & amp; & amp; mDelegate.onBGARefreshLayoutBeginLoadingMore(this)) {
            mIsLoadingMore = true;
            Log.d(TAG, "run:mIsLoadingMore=" + mIsLoadingMore);
            if (mIsShowLoadingMoreView) {
                showLoadingMoreView();
            }
        }
    }

    /**
     * Show pull-up to load more controls
     */
    public void showLoadingMoreView() {
        mRefreshViewHolder.changeToLoadingMore();
        mLoadMoreFooterView.setVisibility(VISIBLE);
        mLoadMoreFooterView.findViewById(R.id.layout_loading).setVisibility(VISIBLE);
        mLoadMoreFooterView.findViewById(R.id.tv_normal_refresh_footer_status_finish).setVisibility(GONE);

        BGARefreshScrollingUtil.scrollToBottom(mParentNestedScrollView);
        BGARefreshScrollingUtil.scrollToBottom(mScrollView);
        BGARefreshScrollingUtil.scrollToBottom(mRecyclerView);
        BGARefreshScrollingUtil.scrollToBottom(mAbsListView);
        if (mStickyNavLayout != null) {
            mStickyNavLayout.scrollToBottom();
        }
    }

    /**
     * End pull-up to load more
     */
    public void endLoadingMore() {
        if (mIsShowLoadingMoreView) {
            // Avoid requesting data too quickly in a WiFi environment and loading more controls in a flash
            mRecyclerView.postDelayed(mDelayHiddenLoadingMoreViewTask, 300);
        }
    }
    public void setStatusFinish() {
        mLoadMoreFooterView.setVisibility(VISIBLE);
        mLoadMoreFooterView.findViewById(R.id.tv_normal_refresh_footer_status_finish)
                .setVisibility(VISIBLE);
        BGARefreshScrollingUtil.scrollToBottom(mParentNestedScrollView);
        BGARefreshScrollingUtil.scrollToBottom(mScrollView);
        BGARefreshScrollingUtil.scrollToBottom(mRecyclerView);
        BGARefreshScrollingUtil.scrollToBottom(mAbsListView);
        if (mStickyNavLayout != null) {
            mStickyNavLayout.scrollToBottom();
        }
    }

    

The main reason is to add new processing logic for NestedScrollView; it is also to adapt to the following xml layout:

 /**
         * Layout nested as
         * NestedScrollView
         *BGARefreshLayout
         *RecyclerView
         * */

Coupled with BGARefreshScrollingUtil.scrollToBottom(mParentNestedScrollView); processing, the pull-up loading of more data and paging loading logic connection processing are completed.
After optimizing this component, the loading smoothness improved immediately. There is no obvious lag when sliding and loading data within 800 items.
But the problem has not been completely solved, because when the amount of data reaches about 1,000, ANR problems and OOM memory problems appear. Although these two problems have not been completely solved, there are general solutions.
Let’s take a break here first, there will be another article later

Recommended smartApi interface development tools

Reason for recommendation

It has become increasingly difficult to use postman in the country:
1. Serious login problem
2. The Mock function service is basically unusable.
3. The version update function is already lacking.
4. Some external factors make it risky to use postman in the future.
For the above considerations, the author developed an API debugging and development tool SmartApi to meet the basic daily development and debugging API needs.

Introduction

After more than a year and a half of development, the smartApi-v1.0.0 version was officially launched at 10pm on September 15, 2023
smartApi is an API debugging and development tool that benchmarks against foreign postman. Due to limited development manpower, the functions of the v1.0.0 version have been streamlined. The major functional items include:

  • Fill in api parameters
  • api request response data display
  • Share documents in PDF format
  • Mock localization solution
  • API list data localization processing
  • Plus UI polishing

The following is an introduction to the use of smartApi:

Download address:

https://pan.baidu.com/s/1kFAGbsFIk3dDR64NwM5y2A?pwd=csdn