使用Android造了個滾輪控件輪子示例
關(guān)于 Android 實現(xiàn) iOS 上的滾輪選擇效果的控件,到 github 上一搜一大堆,之所以還要造這個輪子,目的是為了更好的學(xué)習(xí)自定義控件,這個控件是幾個月前寫的了,經(jīng)過一段時間的完善,現(xiàn)在開源,順便寫這一篇簡單的介紹文章。
效果如下,錄屏軟件看起來可能有點卡頓,具體可以下載源碼運行:

自定義控件無非是 measure,draw,layout 三個過程,如果要支持手勢動作,那么就再加上 touch 。
measure
測量過程比較簡單,以文本大小所需要的尺寸,再加上 padding。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wantWith = getPaddingLeft() + getPaddingRight();
int wantHeight = getPaddingTop() + getPaddingBottom();
calculateTextSize();
wantWith += mTextRect.width();
//可見 item 數(shù)量計算文本尺寸
if (mVisibilityCount > 0) {
wantHeight += mTextRect.height() * mVisibilityCount;
} else {
wantHeight += mTextRect.height() * DEFALUT_VISIBILITY_COUNT;
}
setMeasuredDimension(
resolveSize(wantWith, widthMeasureSpec),
resolveSize(wantHeight, heightMeasureSpec)
);
mNeedCalculate = true;
}
draw
繪制過程是通過 canvas 的位移去繪制不同位置的部件,包括文本內(nèi)容和選擇框之類的,這里可能需要注意下的地方是,不要一次性把所有文本繪制出來,只需要繪制可見文本即可。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (hasDataSource()) {
// 省略
// 這里計算下需要繪制的數(shù)量,+2 只是確保不會出現(xiàn)空白
final int drawCount = mContentRect.height() / mTextRect.height() + 2;
int invisibleCount = 0;
int dy = -mDistanceY;
// 省略
// 通過 translate 繪制文本
for (int i = 0; (i < drawCount && mDataSources.size() > (invisibleCount + i));
i++) {
final int position = invisibleCount + i;
String text = mDataSources.get(position);
if (i > 0) {
canvas.translate(0, mTextRect.height());
}
final PointF pointF = calculateTextGravity(text);
mTextPaint.setTextSize(mTextSize);
if (position == selctPosition) {
mTextPaint.setColor(mSelectedTextColor);
} else {
mTextPaint.setColor(mNormalTextColor);
}
canvas.drawText(text, pointF.x, pointF.y, mTextPaint);
}
canvas.restoreToCount(saveCount);
}
// 繪制選擇框
int saveCount = canvas.save();
mDrawPaint.setColor(mSelectedLineColor);
canvas.translate(mContentRect.left, mContentRect.top);
canvas.drawLine(
mSelctedRect.left,
mSelctedRect.top,
mSelctedRect.right,
mSelctedRect.top,
mDrawPaint
);
canvas.drawLine(
mSelctedRect.left,
mSelctedRect.bottom,
mSelctedRect.right,
mSelctedRect.bottom,
mDrawPaint
);
canvas.restoreToCount(saveCount);
}
layout
因為這個控件是繼承于 View,所以不需要處理 onLayout。
touch
如果對 touch event 分發(fā)流程熟悉的話,那么很多處理可以說是模版代碼,可以參考 NestedScrollView、ScrollView。
在 onInterceptTouchEvent 中,判斷是否開始進行拖動手勢,保存到變量(mIsBeingDragged)中:
// 多指處理
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
// 開始拖動
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
// 禁止父控件攔截事件分發(fā)
parent.requestDisallowInterceptTouchEvent(true);
}
}
在 onTouchEvent 中對 ACTION_MOVR 進行拖動的處理,如果支持嵌套滾動,那么會預(yù)先進行嵌套滾動的分發(fā)。如果支持陰影效果,那么使用 EdgeEffect。
// 和 onInterceptTouchEvent 一樣進行拖動手勢開始的判斷
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// 拖動處理
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
final int oldY = mScrollY;
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollBy will call onOverScrolled, which
// calls onScrollChanged if applicable.
// 滾動處理,overScrollBy 中會處理嵌套滾動預(yù)先分發(fā)
if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
&& !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = mScrollY - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
// 嵌套滾動
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
final int pulledToY = oldY + deltaY;
// 拖動陰影效果
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight(),
ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
1.f - ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
postInvalidateOnAnimation();
}
}
}
支持滾動手勢的控件,一般都會支持 fling 手勢,可以理解為慣性滾動。這也是模版代碼,在 onTouchEvent 中對 ACTION_UP 中對拖動速度進行分析。
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
// 獲取拖動速度
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
// 可以進行 fling 操作
flingWithNestedDispatch(-initialVelocity);
} else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
具體的代碼可以在 ScrollView 中閱讀。
回到我實現(xiàn)的自定義控件來,對 touch event 的處理代碼可以說是和系統(tǒng)控件的處理沒有什么兩樣,在獲取到拖動的距離后,根據(jù)這個值繪制不同位置的可見區(qū)域。這里多了兩個處理是:
第一拖動結(jié)束后,進行復(fù)位處理。拖動結(jié)束后,選擇框如果停留在兩個 item 之間,那么根據(jù)和兩個 item 的距離進行比較,選擇更近的 item。
private void correctionDistanceY() {
if (mDistanceY % mTextRect.height() != 0) {
int position = mDistanceY / mTextRect.height();
int remainder = mDistanceY % mTextRect.height();
if (remainder >= mTextRect.height() / 2f) {
position++;
}
int newDistanceY = position * mTextRect.height();
animChangeDistanceY(newDistanceY);
}
}
第二個是在使用上發(fā)現(xiàn)的問題,如果剩余可滾動的距離過短,拖動的手勢速度又很快,就會導(dǎo)致 fling 處理沒結(jié)束,視覺上又沒有改變,同時是在滾動結(jié)束后才進行選擇的回調(diào),所以體檢上不好,但是 Scroller 并沒有提供 setDuration,所以拷貝 Scroller 中計算 duration 的方法,根據(jù)剩余的滾動計算合適的 duration,手動中斷 Scroller 的 fling 處理。
if ((SystemClock.elapsedRealtime() - mStartFlingTime) >= mFlingDuration || currY == mScroller.getFinalY()) {
//duration or current == final
if (DEBUG) {
Logger.d("abortAnimation");
}
mScroller.abortAnimation();
}
具體的代碼可以閱讀源碼。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
ListView異步加載圖片實現(xiàn)思路(優(yōu)化篇)
關(guān)于listview的異步加載,網(wǎng)上其實很多示例了,中心思想都差不多,不過很多版本或是有bug,或是有性能問題有待優(yōu)化,下面就讓在下闡述其原理以探索個中奧秘2013-04-04
Android學(xué)習(xí)筆記之應(yīng)用單元測試實例分析
這篇文章主要介紹了Android學(xué)習(xí)筆記之應(yīng)用單元測試,結(jié)合實例形式較為詳細(xì)的分析了Android單元測試的實現(xiàn)原理與具體步驟,具有一定參考借鑒價值,需要的朋友可以參考下2015-11-11
解決video標(biāo)簽在安卓webview下無法自動播放問題
這篇文章主要介紹了video標(biāo)簽在安卓webview下無法自動播放問題的解決方法 ,需要的朋友可以參考下2014-03-03

