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

写在开始之前

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

  • 色调: 图像的颜色

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

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

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

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

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

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

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

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