在Android开发中,Seekbar是我们高频率使用的几个控件之一,不管是使用原生系统的样式,还是根据各种五花八门的需求来定制自己项目中的SeekBar(在这里吐槽下项目经理),相信大家都做过,那么闲话不说进入正题。
SeekBar的继承关系为:
有些相对比较麻烦的需求要重写SeekBar,甚至有时候要直接写个重写继承View的SeekBar,但又想保留SeekBar的一些特性,比如我们今天要讲的fromUser参数,这个参数的作用是触发SeekBar的onProgressChanged回调接口时,可以根据这个参数判断是手动滑动SeekBar还是其他的一些方式改变了SeekBar的值。如果我们重写的自定义View要做这个功能,就要去了解这个参数的调用流程是怎样的,我们先来看Seekbar里的onProgressChanged是怎样被调用的,
/ frameworks / base / refs/heads/master / . / core / java / android / widget / SeekBar.java
public SeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
//回调的地方
@Override
void onProgressRefresh(float scale, boolean fromUser, int progress) {
super.onProgressRefresh(scale, fromUser, progress);
if (mOnSeekBarChangeListener != null) {
mOnSeekBarChangeListener.onProgressChanged(this, progress, fromUser);
}
}
可以看到onProgressChanged的fromUser参数是这个onProgressRefresh方法传过来的,那么这个onProgressRefresh是在哪里调用的呢?我们先去SeekBar的父类里看看,
/ frameworks / base / refs/heads/master / . / core / java / android / widget / AbsSeekBar.java
啊咧??找不到,在它的父类里完全找不到这个方法,那么我们再去AbsSeekBar的父类也就是ProgressBar里找找看,
....
private synchronized void doRefreshProgress(int id, int progress, boolean fromUser,
boolean callBackToApp, boolean animate) {
final float scale = mMax > 0 ? progress / (float) mMax : 0;
final boolean isPrimary = id == R.id.progress;
if (isPrimary && animate) {
final ObjectAnimator animator = ObjectAnimator.ofFloat(this, VISUAL_PROGRESS, scale);
animator.setAutoCancel(true);
animator.setDuration(PROGRESS_ANIM_DURATION);
animator.setInterpolator(PROGRESS_ANIM_INTERPOLATOR);
animator.start();
} else {
setVisualProgress(id, scale);
}
if (isPrimary && callBackToApp) {
//注意这里
onProgressRefresh(scale, fromUser, progress);
}
}
....
哈哈,找到了吧,原来这个onProgressRefresh是在SeekBar的祖父类里的doRefreshProgress被调用的,这个fromUser也是被这个方法传过来的,可以看到这个方法是private且是加了锁的,我们再往上追溯,还是在ProgressBar里,
....
private synchronized void refreshProgress(int id, int progress, boolean fromUser,
boolean animate) {
if (mUiThreadId == Thread.currentThread().getId()) {
//注意这里
doRefreshProgress(id, progress, fromUser, true, animate);
} else {
if (mRefreshProgressRunnable == null) {
mRefreshProgressRunnable = new RefreshProgressRunnable();
}
final RefreshData rd = RefreshData.obtain(id, progress, fromUser, animate);
mRefreshData.add(rd);
if (mAttached && !mRefreshIsPosted) {
post(mRefreshProgressRunnable);
mRefreshIsPosted = true;
}
}
}
....
还是在ProgressBar里,再次向上追溯,
....
@android.view.RemotableViewMethod
synchronized boolean setProgressInternal(int progress, boolean fromUser, boolean animate) {
if (mIndeterminate) {
// Not applicable.
return false;
}
progress = MathUtils.constrain(progress, 0, mMax);
if (progress == mProgress) {
// No change from current.
return false;
}
mProgress = progress;
//注意这里
refreshProgress(R.id.progress, mProgress, fromUser, animate);
return true;
}
....
原来是在setProgressInternal这里被调用的,看方法名字就知道,意思是内部设置进度值,我们再看看这个方法是在哪里被调用的,
....
/**
* Sets the current progress to the specified value. Does not do anything
* if the progress bar is in indeterminate mode.
*
* This method will immediately update the visual position of the progress
* indicator. To animate the visual position to the target value, use
* {@link #setProgress(int, boolean)}}.
*
* @param progress the new progress, between 0 and {@link #getMax()}
*
* @see #setIndeterminate(boolean)
* @see #isIndeterminate()
* @see #getProgress()
* @see #incrementProgressBy(int)
*/
@android.view.RemotableViewMethod
public synchronized void setProgress(int progress) {
//注意这里
setProgressInternal(progress, false, false);
}
....
看到这个方法是不是很熟悉啊?我们再用SeekBar时是不是经常调这个方法,这个方法实际上就调用了setProgressInternal方法,这个方法的第二个参数就是我们今天要说的fromUser参数,而且这个参数的源头就是由setProgressInternal来最终分发的,一层层一直传到我们的onProgressChanged里,看到这里是不是已经有点想法了呢?是不是直接设置值的时候穿false,那什么时候传true呢?答案就是我们直接点击Seekbar或者拖动的时候,只要有点击拖动那便会想到onTouchEvent吧,那么我们再去看看这个ProgressBar的onTouchEvent里,
…很可惜ProgressBar并没有重写这个方法,那我们再去SeekBar的父类AbsSeekBar里找找看,
....
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mIsUserSeekable || !isEnabled()) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isInScrollingContainer()) {
mTouchDownX = event.getX();
} else {
startDrag(event);
}
break;
case MotionEvent.ACTION_MOVE:
if (mIsDragging) {
//注意这里
trackTouchEvent(event);
} else {
final float x = event.getX();
if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) {
startDrag(event);
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsDragging) {
//注意这里
trackTouchEvent(event);
onStopTrackingTouch();
setPressed(false);
} else {
// Touch up when we never crossed the touch slop threshold should
// be interpreted as a tap-seek to that location.
onStartTrackingTouch();
//注意这里
trackTouchEvent(event);
onStopTrackingTouch();
}
// ProgressBar doesn't know to repaint the thumb drawable
// in its inactive state when the touch stops (because the
// value has not apparently changed)
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
if (mIsDragging) {
onStopTrackingTouch();
setPressed(false);
}
invalidate(); // see above explanation
break;
}
return true;
}
private void startDrag(MotionEvent event) {
setPressed(true);
if (mThumb != null) {
// This may be within the padding region.
invalidate(mThumb.getBounds());
}
onStartTrackingTouch();
//注意这里
trackTouchEvent(event);
attemptClaimDrag();
}
....
果不其然找到了,注意这个类重写的onTouchEvent方法,很多地方都调用了这个trackTouchEvent(event)方法,我们再去看看这个方法干了什么事,
....
private void trackTouchEvent(MotionEvent event) {
final int x = Math.round(event.getX());
final int y = Math.round(event.getY());
final int width = getWidth();
final int availableWidth = width - mPaddingLeft - mPaddingRight;
final float scale;
float progress = 0.0f;
if (isLayoutRtl() && mMirrorForRtl) {
if (x > width - mPaddingRight) {
scale = 0.0f;
} else if (x < mPaddingLeft) {
scale = 1.0f;
} else {
scale = (availableWidth - x + mPaddingLeft) / (float) availableWidth;
progress = mTouchProgressOffset;
}
} else {
if (x < mPaddingLeft) {
scale = 0.0f;
} else if (x > width - mPaddingRight) {
scale = 1.0f;
} else {
scale = (x - mPaddingLeft) / (float) availableWidth;
progress = mTouchProgressOffset;
}
}
final int max = getMax();
progress += scale * max;
setHotspot(x, y);
//注意这里
setProgressInternal(Math.round(progress), true, false);
}
....
答案已经揭晓,这个trackTouchEvent最后就直接调用了我们ProgressBar里的setProgressInternal方法,而且注意第二个参数也就是我们的fromUser参数,传的是true!一切已然揭晓,在onTouchEvent的话说明是手动调节,理应传入true,在其他地方改变的话直接传false就好了,系统就是这么玩的,同学们明白了么?