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);
}
}
最終效果圖如下:
以下是原始碼地址,部分程式碼和部落格中的可能有所出入: