0
点赞
收藏
分享

微信扫一扫

实现监听滚动状态、滚动页码、滚动到最底部的ScrollView /android

归零者245号 2022-03-25 阅读 47
androidjava

实现监听滚动状态、滚动页码、滚动到最底部的ScrollView/android

提出问题

最近项目业务上有这样一个需求,原生实现阅读交易协议长文本,要求显示页码,阅读完成才能高亮同意按钮。采用ScrollView + TextView的方案只能实现展示和滚动阅读,对于页码和是否滚动到最底部的需求,需要进行扩展实现。

分析问题

我们知道ScrollView最多只能包含一个直接孩子,而且自身也有滚动回调方法,所以我们可以在滚动回调方法中做文章。,

解决问题

先看效果图:
在这里插入图片描述

下面直接上代码:

自定义ScrollView:

package com.bg.blogcodeproject.widget.scroll;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.widget.NestedScrollView;

/**
 * @Desc: 带滚动监听的ScrollView:
 * 监听实时滚动位置,
 * 监听滚动状态变化、
 * 监听滚动页码变化、
 * 监听是否滚动到底部
 * @Author:bg
 * @Date: 2022/3/22 17:00
 */
public class ObservableScrollView extends NestedScrollView {

    /**
     * 每隔80毫秒检查一次滚动状态变化
     */
    private static final int CHECK_SCROLL_DELAY_MILLIS = 80;
    private static final int MSG_SCROLL = 2468;

    private static final String TAG = "ObservableScrollView";

    /**
     * 当前是否为触摸状态
     */
    private boolean mTouched = false;

    /**
     * 当前是否滚动到底部
     */
    private boolean mScrolledEnd = false;

    /**
     * 当前页码
     */
    private int mPage = 1;

    /**
     * 总页码
     */
    private int mTotalPage = 0;

    /**
     * 判断是否滑动到下一页的比例,取值范围区间(0,1)
     */
    private static final float NEXT_PAGE_RATIO = 0.5f;

    /**
     * 记录当前的滚动状态
     */
    private ScrollState mScrollState = ScrollState.IDLE;

    private OnScrollStateChangedListener mOnScrollStateChangedListener;

    private OnScrolledEndChangedListener mOnScrolledEndChangedListener;

    private OnScrollPageNumChangedListener mOnScrollPageNumChangedListener;

    public ObservableScrollView(@NonNull Context context) {
        this(context, null);
    }

    public ObservableScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ObservableScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // 视图树布局完成时计算是否滚动到底的初始状态
        getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                // 计算是否滚动到底部
                computeScrolledEnd(ObservableScrollView.this.getScrollY());
                // 计算总页数
                computeTotalPage();
                // 移除视图树布局监听器
                getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
    }

    /**
     * 计算是否滚动到底
     * 1,ScrollView高度使用match_parent
     * 1,ScrollView高度使用wrap_content
     * 1,ScrollView高度写死dp
     *
     * @param scrollY 滚动位置
     */
    private void computeScrolledEnd(int scrollY) {
        // ScrollView有且只有一个孩子
        View child = getChildAt(0);
        int childHeight = child.getHeight();
        // ScrollView自身高度
        int height = getHeight();
        // ScrollView自身高度 + scrollY 达到孩子的高度,就是滚动到底了
        boolean scrolledEnd = height + scrollY >= childHeight;
        setScrolledEnd(scrolledEnd);
    }

    private void setScrolledEnd(boolean end) {
        mScrolledEnd = end;
        Log.i(TAG, "mScrolledEnd = " + mScrolledEnd);
        if (mOnScrolledEndChangedListener != null) {
            mOnScrolledEndChangedListener.onScrolledEnd(mScrolledEnd);
        }
    }

    private void computeTotalPage() {
        // ScrollView有且只有一个孩子
        View child = getChildAt(0);
        // 总高度就是孩子内容的高度
        int totalHeight = child.getHeight();
        Log.i(TAG, "totalPageHeight = " + totalHeight);
        // 单页的高度就是ScrollView自身高度
        int pageHeight = getHeight();
        Log.i(TAG, "pageHeight = " + pageHeight);
        // 计算总页码,向上取整
        int totalPage = (int) Math.ceil(totalHeight * 1.0f / pageHeight);
        setPageNumChanged(mPage, totalPage);
    }

    private void setPageNumChanged(int page, int totalPage) {
        if (mPage != page || mTotalPage != totalPage) {
            mPage = page;
            mTotalPage = totalPage;
            Log.i(TAG, "mPage = " + mPage);
            Log.i(TAG, "mTotalPage = " + mTotalPage);
            if (mOnScrollPageNumChangedListener != null) {
                mOnScrollPageNumChangedListener.onScrollPageNumChanged(mPage, mTotalPage);
            }
        }
    }

    /**
     * 滚动事件回调
     *
     * @param l    Current horizontal scroll origin.
     * @param t    Current vertical scroll origin.
     * @param oldl Previous horizontal scroll origin.
     * @param oldt Previous vertical scroll origin.
     */
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        //计算滚动状态
        computeScrollStateOnScroll(t, oldt);
        //计算是否滚动到底部
        computeScrolledEnd(t);
        // 计算当前页码
        computeScrollPage(t);
        // 滚动位置回调
        setScrollPositionChanged(l, t, oldl, oldt);
    }

    private void computeScrollStateOnScroll(int t, int oldt) {
        int scrolledY = t - oldt;
        if (mTouched) {
            setScrollState(scrolledY > 0 ? ScrollState.TOUCH_SCROLL_DOWN :
                    ScrollState.TOUCH_SCROLL_UP);
        } else {
            setScrollState(scrolledY > 0 ? ScrollState.FLING_DOWN :
                    ScrollState.FLING_UP);
            restartCheckTimer();
        }
    }

    private void setScrollState(ScrollState state) {
        if (state != mScrollState) {
            mScrollState = state;
            Log.i(TAG, "mScrollState = " + mScrollState);
            if (mOnScrollStateChangedListener != null) {
                mOnScrollStateChangedListener.onScrollStateChanged(mScrollState);
            }
        }
    }

    private void restartCheckTimer() {
        mHandler.removeMessages(MSG_SCROLL);
        mHandler.sendEmptyMessageDelayed(MSG_SCROLL, CHECK_SCROLL_DELAY_MILLIS);
    }

    private void computeScrollPage(int scrollY) {
        // 单页的高度就是ScrollView自身高度
        int pageHeight = getHeight();
        float result = scrollY * 1.0f / pageHeight + 1.0f;
        int page;
        if (result > mTotalPage - 1) {
            // 最后一页需要单独判断,存在不满一屏的情况
            // TODO: 2022/3/22 还没想到更好办法,先简单粗暴处理
            page = mTotalPage;
        } else {
            // 其他页,四舍五入,下一页滚动到50%认为已到下一页
            page = (int) (result + NEXT_PAGE_RATIO);
        }
        setPageNumChanged(page, mTotalPage);
    }

    private void setScrollPositionChanged(int l, int t, int oldl, int oldt) {
        Log.i(TAG, "scrollY = " + t);
        if (mOnScrollStateChangedListener != null) {
            mOnScrollStateChangedListener.onScrollChanged(l, t, oldl, oldt);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            handleDownEvent();
        }
        return super.onInterceptTouchEvent(ev);
    }

    private void handleDownEvent() {
        // 手指按下时,为触摸状态
        mTouched = true;
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                handleUpEvent();
                break;
            default:
        }
        return super.onTouchEvent(ev);
    }

    private void handleUpEvent() {
        // 手指抬起时,不再是触摸状态
        mTouched = false;
        restartCheckTimer();
    }

    @Override
    protected void onDetachedFromWindow() {
        mHandler.removeCallbacksAndMessages(null);
        super.onDetachedFromWindow();
    }

    public void setOnScrollStateChangedListener(OnScrollStateChangedListener listener) {
        this.mOnScrollStateChangedListener = listener;
    }

    public void setOnScrollToEndChangedListener(OnScrolledEndChangedListener listener) {
        this.mOnScrolledEndChangedListener = listener;
    }

    public void setOnScrollPageNumChangedListener(OnScrollPageNumChangedListener listener) {
        this.mOnScrollPageNumChangedListener = listener;
    }

    private final Handler mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {

        /**
         * 记录上一次滚动的位置
         */
        private int mLastY = Integer.MIN_VALUE;

        /**
         * @param msg A {@link Message Message} object
         * @return True if no further handling is desired
         */
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            if (MSG_SCROLL == msg.what) {
                int scrollY = getScrollY();
                // 未触摸未发生滚动,就是空闲状态
                if (!mTouched && mLastY == scrollY) {
                    mLastY = Integer.MIN_VALUE;
                    setScrollState(ScrollState.IDLE);
                } else {
                    // 触摸或正在滚动,检测滚动状态
                    mLastY = scrollY;
                    restartCheckTimer();
                }
                return true;
            }
            return false;
        }
    });

    public enum ScrollState {

        /**
         * 停止滚动,空闲静止状态
         */
        IDLE,

        /**
         * 向上触摸滚动,手指触摸滚动状态
         */
        TOUCH_SCROLL_UP,

        /**
         * 向下触摸滚动,手指触摸滚动状态
         */
        TOUCH_SCROLL_DOWN,

        /**
         * 向上飞滚,手指离开后惯性滚动状态
         */
        FLING_UP,

        /**
         * 向下飞滚,手指离开后惯性滚动状态
         */
        FLING_DOWN
    }

    public interface OnScrollStateChangedListener {

        /**
         * 滚动状态变化的回调
         *
         * @param state 滚动状态
         */
        void onScrollStateChanged(ScrollState state);

        /**
         * 滚动事件回调
         *
         * @param x    Current horizontal scroll origin.
         * @param y    Current vertical scroll origin.
         * @param oldX Previous horizontal scroll origin.
         * @param oldY Previous vertical scroll origin.
         */
        void onScrollChanged(int x, int y, int oldX, int oldY);
    }

    public interface OnScrolledEndChangedListener {

        /**
         * 是否滚动到底的回调
         *
         * @param end 是否滚动到底
         */
        void onScrolledEnd(boolean end);
    }


    public interface OnScrollPageNumChangedListener {

        /**
         * 页码变化的回调
         *
         * @param pages      当前页码
         * @param totalPages 总页码
         */
        void onScrollPageNumChanged(int pages, int totalPages);
    }


}

在Activity中的使用如下,布局文件很简单就不罗列出来了:

package com.bg.blogcodeproject;

import android.os.Bundle;
import android.util.Log;

import com.bg.blogcodeproject.widget.scroll.ObservableScrollView;

import java.util.Locale;

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.AppCompatTextView;

public class CustomScrollActivity extends AppCompatActivity implements
        ObservableScrollView.OnScrollStateChangedListener,
        ObservableScrollView.OnScrolledEndChangedListener,
        ObservableScrollView.OnScrollPageNumChangedListener {

    private static final String TAG = "CustomScrollActivity";

    private AppCompatTextView tvPages;
    private AppCompatTextView tvScrollY;
    private AppCompatButton btnEnd;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_custom_scroll);

        tvPages = findViewById(R.id.tv_pages);
        tvScrollY = findViewById(R.id.tv_scroll);
        btnEnd = findViewById(R.id.btn_end);

        ObservableScrollView osv = findViewById(R.id.osv_container);
        osv.setOnScrollStateChangedListener(this);
        osv.setOnScrollToEndChangedListener(this);
        osv.setOnScrollPageNumChangedListener(this);
    }

    /**
     * 滚动状态变化的回调
     *
     * @param state 滚动状态
     */
    @Override
    public void onScrollStateChanged(ObservableScrollView.ScrollState state) {
        Log.i(TAG, "onScrollStateChanged: " + state.name());
    }

    /**
     * 滚动事件回调
     */
    @Override
    public void onScrollChanged(int x, int y, int oldX, int oldY) {
        tvScrollY.setText(String.format(Locale.CHINA, "滚动位置:%d", y));
    }

    /**
     * 是否滚动到底的回调
     *
     * @param end 是否滚动到底
     */
    @Override
    public void onScrolledEnd(boolean end) {
        btnEnd.setAlpha(end ? 1f : 0.3f);
    }

    /**
     * 页码变化的回调
     *
     * @param pages      当前页码
     * @param totalPages 总页码
     */
    @Override
    public void onScrollPageNumChanged(int pages, int totalPages) {
        tvPages.setText(String.format(Locale.CHINA, "页码:%d / %d", pages, totalPages));
    }
}
举报

相关推荐

0 条评论