1. 程式人生 > >Android:如何給ScrollView新增滑塊滾動條

Android:如何給ScrollView新增滑塊滾動條

說到滾動控制元件廣大開發者朋友們想到的無非就是 ListView 和 ScrollView 兩大控制元件,對於前者而言新增滑塊無非是 XML 裡面一句話的事情,但是對於後者而言就沒那麼容易了,至少筆者至今並沒有找到簡單的解決方案。

也罷,沒有輪子的話就自己造一個。

思路的話比較簡單粗暴:使用 SeekBar 作為滑塊繫結到 ScrollView 上。

垂直的 SeekBar

要給 ScrollView 新增滑塊的話 SeekBar 必須是垂直的,這裡筆者使用的是 GitHub 上的 android-verticalseekbar

當然,如果你想要給HorizontalScrollView新增滑塊的話原生的SeekBar就夠了。

ScrollView 的監聽和滾動範圍

大部分的控制元件都提供監聽者模式的回撥,然而不知道為什麼 ScrollView 本身沒有暴露監聽滾動的方法。寫個子類開發介面也是可以的,不過筆者這裡是直接使用了 Google 官方提供的 NestScrollView。

我們先分析一下其回撥介面:

public interface OnScrollChangeListener {
        void onScrollChange(NestedScrollView v, int scrollX, int scrollY,int oldScrollX, int oldScrollY);
}

其中 scrollY 是當前的滾動位置,和可滾動範圍的關係圖解如下:

可滾動範圍的圖解

理解了這一層關係之後要做的就很簡單了,我們將可滾動範圍和 scrollY 對映到 SeekBar 上即可。

滾動繫結

明白了邏輯之後我們只需要將 ScrollView 的滾動對映到 SeekBar,再將 SeekBar 的使用者拖動映射回 ScrollView 就行了。以下是筆者封裝了的一個輔助類,註釋比較詳盡:

public class ScrollBindHelper implements SeekBar.OnSeekBarChangeListener, NestedScrollView.OnScrollChangeListener
{
private final SeekBar seekBar; private final NestedScrollView scrollView; private final View scrollContent; //使用靜態方法繫結並返回物件 public static ScrollBindHelper bind (SeekBar seekBar, NestedScrollView scrollView) { ScrollBindHelper helper = new ScrollBindHelper(seekBar, scrollView); seekBar.setOnSeekBarChangeListener(helper); scrollView.setOnScrollChangeListener(helper); return helper; } private ScrollBindHelper (SeekBar seekBar, NestedScrollView scrollView) { this.seekBar = seekBar; this.scrollView = scrollView; this.scrollContent = scrollView.getChildAt(0); } //使用者是否正在拖動SeekBar的標誌 private boolean isUserSeeking; //獲取滾動範圍 private int getScrollRange () { return scrollContent.getHeight() - scrollView.getHeight(); } @Override public void onScrollChange (NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { //使用者拖動SeekBar時不觸發ScrollView的回撥 if (isUserSeeking) {return;} //計算當前滑動位置相對於整個範圍的百分比,並對映到SeekBar上 int range = getScrollRange(); seekBar.setProgress(range != 0 ? scrollY * 100 / range : 0); } @Override public void onProgressChanged (SeekBar seekBar, int progress, boolean fromUser) { //當不是使用者操作,也就是ScrollView的滾動隱射過來時不執行操作 if (!fromUser) { return;} //將拖動的百分比換算成Y值,並對映到SrollView上。 scrollView.scrollTo(0, progress * getScrollRange() / 100); } @Override public void onStartTrackingTouch (SeekBar seekBar) { //標記使用者正在拖動SeekBar isUserSeeking = true; } @Override public void onStopTrackingTouch (SeekBar seekBar) { //標記使用者已經不再操作SeekBar isUserSeeking = false; } }

可見性和動畫

我們知道滾動檢視的滑塊並不是一直存在的,它有著如下的行為:

  • 預設不可見
  • 內容檢視高度太小,比如小於三個螢幕高度時不出現滑塊
  • 出現之後介面滾動或者滑塊被使用者觸控時滑塊不會消失
  • 停止操作若干毫秒後消失

可見性的切換我們只要切換滑塊控制元件的 Visible 屬性即可而滾動和觸控都在我們的回撥之中,最後停止操作若干秒後消失可以直接使用一個 handler 搞定。我們建立一個只響應最後一次操作的 Handler 基類。

//只響應最後一次操作的基類
public abstract class LastMsgHandler extends Handler {

    //標記是第幾次count
    private int count = 0;

    /**
     * 增加Count數。
     */
    public synchronized final void increaseCount() {
        count++;
    }

    //直接傳送訊息
    public final void sendMsg() {
        sendMsgDelayed(0);
    }

    //增加count數後傳送延時訊息
    //如果延時小於或者等於0則直接傳送。
    public final void sendMsgDelayed(long delay) {
        increaseCount();
        if (delay <= 0) {
            sendEmptyMessage(0);
        } else {
            sendEmptyMessageDelayed(0, delay);
        }
    }

    //清空所有count和訊息
    public synchronized final void clearAll() {
        count = 0;
        removeCallbacksAndMessages(null);
    }

    @Override
    public synchronized final void handleMessage(Message msg) {
        super.handleMessage(msg);
        count--;

        //確保count數不會異常
        if (count < 0) {
            throw new IllegalStateException("count數異常");
        }

        //當count為0時說明是最後一次請求
        if (count == 0) {
            handleLastMessage(msg);
        }
    }

    //響應最後一次請求
    protected abstract void handleLastMessage(Message msg);
}

之後我們繼承該類實現最後一次響應請求時切換滑塊可見性的方法:

private static class VisibleHandler extends LastMsgHandler {

    public static final long DEFAULT_TIME_OUT = 1000L;      

    private ScrollBindHelper helper;

    public VisibleHandler(ScrollBindHelper helper) {
        this.helper = helper;
    }

    public void reset() {
        sendMsgDelayed(DEFAULT_TIME_OUT);
    }

    @Override
    protected void handleLastMessage(Message msg) {
        helper.hideScroll();
    }
}

之後只需要將計時器安插進原有的回撥即可。最後給出完整版的ScrollHelper。

public class ScrollBindHelper implements SeekBar.OnSeekBarChangeListener, NestedScrollView.OnScrollChangeListener {

    private final SeekBar seekBar;
    private final NestedScrollView scrollView;
    private final View scrollContent;

    /**
     * 使用靜態方法來繫結邏輯,程式碼可讀性更高。
     */
    public static ScrollBindHelper bind(SeekBar seekBar, NestedScrollView scrollView) {
        ScrollBindHelper helper = new ScrollBindHelper(seekBar, scrollView);
        seekBar.setOnSeekBarChangeListener(helper);
        scrollView.setOnScrollChangeListener(helper);
        return helper;
    }

    private ScrollBindHelper(SeekBar seekBar, NestedScrollView scrollView) {
        this.seekBar = seekBar;
        this.scrollView = scrollView;
        this.scrollContent = scrollView.getChildAt(0);
    }

    /*繼承*/

    private boolean isUserSeeking;

    private int getContentRange() {
        return scrollContent.getHeight();
    }

    private int getScrollRange() {
        return scrollContent.getHeight() - scrollView.getHeight();
    }

    @Override
    public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
        //使用者觸控時不觸發
        if (isUserSeeking) {
            return;
        } else if (getContentRange() < ViewUtil.getScreenHeightPx() * 3) {//寬度小於三個螢幕不做處理
            return;
        }

        int range = getScrollRange();
        seekBar.setProgress(range != 0 ? scrollY * 100 / range : 0);
    }

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        showScroll();

        if (!isUserSeeking) {
            handler.reset();
        }

        //不是使用者操作的時候不觸發
        if (!fromUser) {
            return;
        }

        scrollView.scrollTo(0, progress * getScrollRange() / 100);
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
        isUserSeeking = true;
        handler.clearAll();
    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
        isUserSeeking = false;
        handler.reset();
    }

    /*動畫*/

    public static final long DEFAULT_TIME_OUT = 1000L;

    private static class VisibleHandler extends WeakRefLastMsgHandler<ScrollBindHelper> {

        public VisibleHandler(ScrollBindHelper ref) {
            super(ref);
        }

        public void reset() {
            sendMsgDelayed(DEFAULT_TIME_OUT);
        }

        @Override
        protected void onLastMessageLively(@NonNull ScrollBindHelper ref, Message msg) {
            ref.hideScroll();
        }
    }

    private VisibleHandler handler = new VisibleHandler(this);

    private void hideScroll() {
        ViewUtil.hideWithAnim(seekBar, 0);
    }

    private void showScroll() {
        ViewUtil.showWithAnim(seekBar, 0);
    }

}

最終效果圖如下:

這裡寫圖片描述

以下是原始碼地址,部分程式碼和部落格中的可能有所出入: