android 上拖拉底部,Android拖动条(SeekBar)简单源码剖析

写在开始之前

在Android的色彩处理中,我们通常用三个角度来描述一个图像:

色调: 图像的颜色

饱和度:颜色的纯度,从0(灰)到100%(饱和)来进行描述

亮度:颜色的相对明暗程度

在上面三个属性中,饱和度和亮度为0会使得图片看起来是纯黑色。(记住这一点)

本篇源码分析的原因就是来自这个问题。

正文

在Android开发的过程中,大家有可能都使用过SeekBar这个控件,比如拖动视频进度条、音频进度条等。不管大家用的多还是少,由于工作原因,个人用到的还是比较少的。然后最近在看书的时候,书中为了直观的展示颜色矩阵(ColorMatrix)的变换,有一段代码是通过SeekBar拖动来实时修改图像。

demo的样式就是下图展示的这样:

AAffA0nNPuCLAAAAAElFTkSuQmCC

然后这段代码也很简单

实现一个OnSeekBarChangeListener接口;

给SeekBar设置setOnSeekBarChangeListener()的监听;

重写onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)方法即可。

事实上,我们实际开发过程中也是这样处理的。

那么看下面代码:float mHue, mSaturation, mLum;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.app_activity_layout_color_matrix);

...

//省略初始化控件代码

...

initSeekBarProperty();

}

/**

* 初始化进度条属性

*/

private void initSeekBarProperty() {

mSeekbarHue.setProgress(MID_VALUE);

mSeekbarSaturation.setProgress(MID_VALUE);

mSeekbarScale.setProgress(MID_VALUE);

mSeekbarHue.setOnSeekBarChangeListener(this);

mSeekbarSaturation.setOnSeekBarChangeListener(this);

mSeekbarScale.setOnSeekBarChangeListener(this);

}

@Override

public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {

if (seekBar == mSeekbarHue) { //调整色调,色调范围在-180~180之间一个周期

mHue = (progress - MID_VALUE) * 1.0f / MID_VALUE * 180;

} else if (seekBar == mSeekbarSaturation) { //调整饱和度

mSaturation = progress * 1.0f / MID_VALUE;

} else if (seekBar == mSeekbarScale) { //调整亮度

mLum = progress * 1.0f / MID_VALUE;

}

mImageMatrix.setImageBitmap(handleImageMatrix(bitmap, mHue, mSaturation, mLum));

}

在initSeekBarProperty()方法中为Seekbar设置当前要显示的进度,并且设置进度条改变的监听。

然后运行Demo,只拖动控制色调的Seekbar,是不是以为大功告成了?我发现此时图片变成了黑色。

想到我们在上面的拓展,当饱和度和亮度为0时,图片是会变成黑色背景。那么我们调试一下代码,来验证下是不是这样,调试代码如图:

AAffA0nNPuCLAAAAAElFTkSuQmCC

调试验证了我们的猜想。

从网上搜索答案,解决方案是:

将setOnSeekBarChangeListener()监听放在setProgress()之前

然后将设置监听和设置进度的顺序调换了一下,果真没有问题了。

源码部分

那么真正的原因是什么呢?我们从源码的角度来简单剖析一下这个问题。

首先说一下Seekbar的继承关系:

AAffA0nNPuCLAAAAAElFTkSuQmCC

首先看一下监听回调的方法:

void onProgressRefresh(float scale, boolean fromUser, int progress)

进入SeekBar的源码,可以看到onProgressRefresh()方法源码如下:

@Override

void onProgressRefresh(float scale, boolean fromUser, int progress) {

super.onProgressRefresh(scale, fromUser, progress);

if (mOnSeekBarChangeListener != null) {

mOnSeekBarChangeListener.onProgressChanged(this, progress, fromUser);

}

}

SeekBar的onProgressRefresh()方法里面是先执行了父类的onProgressRefresh()方法,先看AbsSeekBar,在AbsSeekBar中是没有onProgressRefresh()方法的,说明SeekBar执行的是ProgresssBar中的onProgressRefresh()方法,

源码如下:

void onProgressRefresh(float scale, boolean fromUser, int progress) {

if (AccessibilityManager.getInstance(mContext).isEnabled()) {

scheduleAccessibilityEventSender();

}

}

看一下,哪些地方调用了这个方法,发现只有在doRefreshProgress()方法中被调用,看一下这个方法的源码如下:

private synchronized void doRefreshProgress(int id, int progress, boolean fromUser,

boolean callBackToApp, boolean animate) {

...

省略部分源码

...

if (isPrimary && callBackToApp) {

onProgressRefresh(scale, fromUser, progress);

}

}

可以看到在这个方法中,fromUser这个参数也是传过来的,看一下哪些地方调用了该方法。

private synchronized void refreshProgress(int id, int progress, boolean fromUser,

boolean animate) {

if (mUiThreadId == Thread.currentThread().getId()) {

doRefreshProgress(id, progress, fromUser, true, animate);

}

...

省略部分源码

...

}

再查看该方法被调用的地方,可以看到有这个一个方法调用了该方法,源码如下:

@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);

}

看到这里,终于看到了一个熟悉的方法,这个setProgress()就是我们在初始化的时候给seekbar设置当前进度的方法,这个方法实际上就调用了setProgressInternal()方法。第二个fromUser参数就是通过这个方法一层层分发下去。然后看到这个值为false,你会不会有点想法:什么时候这个值为true呢?

答案就是当我们拖动seekbar的时候。

一提到拖动,你是不是想到了onTouchEvent()事件分发?我们来看一下源码,发现只有在AbsSeekBar中重写了onTouchEvent()方法,源码如下:

@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;

}

这里注意两个方法,startDrag()和trackTouchEvent()

private void startDrag(MotionEvent event) {

setPressed(true);

if (mThumb != null) {

// This may be within the padding region.

invalidate(mThumb.getBounds());

}

onStartTrackingTouch();

trackTouchEvent(event);

attemptClaimDrag();

}

private void trackTouchEvent(MotionEvent event) {

...

省略部分源码

...

setHotspot(x, y);

setProgressInternal(Math.round(progress), true, false);

}

我们发现在startDrag()中也调用了trackTouchEvent()方法,然后可以看到在trackTouchEvent()最后是调用了setProgressInternal()方法去设置seekbar的进度值,并且,这个方法的第二个参数传值为true。

到这里我们基本上就能明白:

当我们通过setProgress()设置进度时,这个时候fromUser传值为false;

当我们拖动seekbar时,fromUser传值为true;

那么回到我们最开始的问题,为什么需要先设置监听呢? 答案在SeekBar的onProgressChanged()方法中if (mOnSeekBarChangeListener != null) {

mOnSeekBarChangeListener.onProgressChanged(this, progress, fromUser);

}

总结

当我们先通过setProgress()设置进度时,此时回调到onProgressChanged()方法时,由于mOnSeekBarChangeListener == null, 所以不会去执行我们重写的onProgressChange()方法,自然也就不会去改变色调、饱和度和亮度这几个的值,由于在初始化的时候,这三个值默认为0.f,然后当饱和度和亮度为0的时候,图片会变成黑色。

如果我们先设置监听,再去通过setProgress()设置进度,此时由于mOnSeekBarChangeListener != null就可以回调到onProgressChanged()方法中修改三个变量的值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值