Android拖动条(SeekBar)简单源码剖析

写在开始之前

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

  • 色调: 图像的颜色

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

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

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

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

正文

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

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

b90d4ee5-5a66-49be-a343-19a3bbf8db62.png

然后这段代码也很简单

  • 实现一个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时,图片是会变成黑色背景。那么我们调试一下代码,来验证下是不是这样,调试代码如图:

调试验证了我们的猜想。

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

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

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

源码部分

那么真正的原因是什么呢?我们从源码的角度来简单剖析一下这个问题。
首先说一下Seekbar的继承关系:

首先看一下监听回调的方法:
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);
        }
    }

SeekBaronProgressRefresh()方法里面是先执行了父类的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.
     * <p>
     * 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;
    那么回到我们最开始的问题,为什么需要先设置监听呢? 答案在SeekBaronProgressChanged()方法中
if (mOnSeekBarChangeListener != null) {
            mOnSeekBarChangeListener.onProgressChanged(this, progress, fromUser);
        }

总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容