
简介自定义SeekBar图片背景图片进度是一份面向Android开发者的完整示例工程主要解决系统自带进度条样式固定、难以匹配应用整体视觉风格的问题资源通过继承系统SeekBar、自定义属性用两张长条形图片分别作为背景和进度条实现了个性化视觉效果适用于音量调节、亮度调节等滑动交互场景也适合有一定Android基础的读者深入学习View的自定义流程。压缩包共52个文件其中22个PNG覆盖多分辨率屏幕7个XML承载布局与属性定义4个Java为关键源码另有APK及编译中间文件整体约696KB目录结构清晰便于导入工程查看运行目前已有849人参与学习下载。工程内含自定义类的完整代码、attrs.xml属性声明、布局调用示例及drawable资源组织覆盖从属性声明、图片加载到缩略图设置的完整流程读者还可在此基础上扩展进度条颜色、形状、动画及触摸反馈灵活适配实际项目需求。 去年做播放器App的时候产品经理甩过来一张设计稿进度条是一根带高光和渐变的金色细线背景是半透明的深灰轨道滑块是一个发光的小圆点。我当时觉得这活儿简单改个颜色的事结果系统自带SeekBar一跑那紫色进度条配上默认滑块跟设计稿差了十万八千里。后来才搞明白SeekBar看起来是个控件本质上就是把背景Drawable、进度Drawable和滑块Drawable叠在一起。只要把背景、进度都换成图片就能做出任意外观的自定义SeekBar。这篇文章就是我当时整个过程的复盘包含素材怎么切、progressDrawable怎么写、clipDrawable怎么裁剪出进度效果以及几个只有做到一半才会遇到的坑。如果你也要做图片背景、图片进度的自定义SeekBar直接按这个思路走能少走不少弯路。1. 先看清SeekBar的绘制原理才有资格改样式1.1 进度条本质上是两层DrawableSeekBar继承自ProgressBar水平模式下它对外暴露的可定制部分其实只有两个progressDrawable和thumb。progressDrawable负责画背景轨道和进度条thumb负责画滑块。progressDrawable通常是一个layer-list里面包含两个关键子层id为android:id/background的背景层id为android:id/progress的进度层。ProgressBar内部维护一个0到max的进度值并把它映射成0到10000的level然后调用progressDrawable.setLevel(level)。也就是说进度条的显示完全靠这个level来控制。这里的关键点在于普通的图片Drawable接收到level变化是不会动的必须借助ClipDrawable或者ScaleDrawable这种能响应level的容器。ClipDrawable可以根据level从0到10000裁剪出对应宽度的内容所以把进度图片包一层ClipDrawable就能天然实现图片跟着进度走的效果。1.2 为什么不能只设置Background不少人第一次尝试是把背景图丢给android:background结果发现背景确实变了但进度条还是系统默认的紫色条。原因是SeekBar的进度层和背景层是两个独立部分setBackground只替换了最底层的背景系统默认的progress层仍然存在会原封不动盖在图片上面。打个比方SeekBar就像一块双层蛋糕底层蛋糕胚是背景上层奶油是进度。你只换了蛋糕胚上面的奶油还是原味整体效果自然不对。正确做法是替换整个progressDrawable让系统默认的progress层不再参与绘制。2. 素材怎么切背景图、进度图、滑块图各自的讲究2.1 尺寸规范与dpi目录选择做图片进度条之前先把素材拆清楚。通常需要三张图seekbar_bg整段轨道背景比如半透明深色圆角条seekbar_progress进度前景比如金色高光条seekbar_thumb滑块图标常见尺寸在36dp到48dp之间以8dp高的轨道为例各dpi目录下的实际像素高度可以参考这个表资源目录屏幕密度8dp对应高度mdpi160dpi12pxhdpi240dpi18pxxhdpi320dpi24pxxxhdpi480dpi36pxxxxhdpi640dpi48px这个表格对设计同学非常友好直接按对应像素出图就行。宽度方面背景图和进度图的宽度可以设计得比实际屏宽大一些反正后面会被裁剪或者拉伸。2.2 进度图两端圆角的处理经验一个容易被忽略的细节进度图被ClipDrawable裁剪时右端会直接变成直角。如果你把进度图设计成两端圆角进度走到50%的时候左侧是圆角、右侧是直角看起来非常奇怪。我推荐的方案是背景图两端做圆角进度图只做左端圆角、右端不做。无论进度走到哪里视觉上都能保持自然。另外如果进度条上有高光、纹理这类容易在拉伸时变形的元素强烈建议使用.9.png。用NinePatch图可以做到只拉伸中间区域圆角、高光纹理都不变形。需要注意的是尽量不要用bitmap标签去包.9.png在某些系统版本上会丢失NinePatch的拉伸规则直接通过layer-list的item引用反而更安全。3. 核心实现layer-list clipDrawable 拼出图片进度条3.1 直接能用的progressDrawable配置在res/drawable/seekbar_progress_drawable.xml里写上这样一份配置背景层引用轨道图进度层用ClipDrawable包住前景图layer-list xmlns:androidhttp://schemas.android.com/apk/res/android item android:idandroid:id/background bitmap android:srcdrawable/seekbar_bg android:gravityfill_horizontal|center_vertical android:tileModedisabled / /item item android:idandroid:id/progress clip android:clipOrientationhorizontal android:gravityleft bitmap android:srcdrawable/seekbar_progress android:gravityfill_horizontal|center_vertical android:tileModedisabled / /clip /item /layer-list这里有两个重力参数需要理解清楚bitmap的android:gravityfill_horizontal|center_vertical作用是让图片在宽度方向填满在高度方向垂直居中不至于被整体拉伸变形clip的android:gravityleft表示进度从左向右展开level值越大裁剪出来的区域越宽如果你的进度图是高保真纹理图我建议把前景图单独包一层layer-list再放进clip里这样能更好地保留NinePatch的拉伸行为item android:idandroid:id/progress clip android:clipOrientationhorizontal android:gravityleft layer-list item android:drawabledrawable/seekbar_progress / /layer-list /clip /item两种写法效果大同小异用之前最好在真机上跑一遍确认纹理拉伸是否符合预期。3.2 Layout中的完整配置与每个属性含义布局文件里完整配置长这样SeekBar android:idid/customSeekBar android:layout_widthmatch_parent android:layout_heightwrap_content android:maxHeight8dp android:minHeight8dp android:progressDrawabledrawable/seekbar_progress_drawable android:thumbdrawable/seekbar_thumb android:thumbOffset0dp android:splitTrackfalse android:progressTintnull android:progressBackgroundTintnull android:thumbTintnull /逐个说一下关键属性maxHeight和minHeight把SeekBar的高度锁定在轨道图的高度避免进度条图片被SeekBar自身测量逻辑拉伸progressDrawable指向刚才写的layer-listthumb滑块Drawable可以直接放普通png也可以用selector做按下状态的切换thumbOffset先设成0dp等看真机效果再微调splitTrackfalse这个很重要后面单独说三个tint属性全部置null防止系统自动给图片上色如果需要在代码里动态设置也可以这样做val drawable ContextCompat.getDrawable(this, R.drawable.seekbar_progress_drawable) seekBar.progressDrawable drawable seekBar.thumb ContextCompat.getDrawable(this, R.drawable.seekbar_thumb) seekBar.progress 30一旦调用setProgressSeekBar会自动更新ClipDrawable的level不用手动干预。4. 真正决定手感的细节thumbOffset、触摸热区与tint4.1 让thumb中心对准进度端点如果换上自定义图片后发现滑块和进度终点对不上基本都是thumbOffset的锅。系统默认的thumbOffset是根据系统默认thumb宽度算出来的换成自定义图片后往往就不匹配了。最省事的方法切thumb图的时候在图标左右各留出约一半宽度的透明区域让视觉中心正好在图片几何中心然后保持thumbOffset0dp。这样进度终点正好落在图标的视觉中心。如果不想留透明边距也可以动态计算偏移量val thumbWidth ContextCompat.getDrawable(this, R.drawable.seekbar_thumb)?.intrinsicWidth ?: 0 seekBar.thumbOffset thumbWidth / 2把这个值当作基准再根据真机效果增减。不同设计师出的图滑块本身的对齐方式不一样没有通用常量必须以实际效果为准。4.2 轨道只有8dp高怎么让用户好拖进度条视觉高度只有8dp触摸热区太小用户想精确拖动会非常痛苦。最常见也是最有效的办法把thumb图做大一点比如36dp到48dp。SeekBar的整体高度会随着thumb的高度扩展但轨道图依然保持8dp视觉上不受影响而整个控件的可点击区域变大了。如果视觉上不允许滑块变大可以做一个36dp的透明png作为触摸热区真正的滑块图标叠加在它中心。说白了就是透明区域用来兜住手指图标负责展示。还有一个思路是用TouchDelegate扩大热区。给SeekBar的父容器设置一个扩大后的触摸代理官方支持也不会破坏SeekBar自身的事件逻辑val delegateArea Rect() seekBar.getHitRect(delegateArea) val expand TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 60f, resources.displayMetrics ).toInt() delegateArea.top - expand delegateArea.bottom expand (seekBar.parent as? ViewGroup)?.touchDelegate TouchDelegate(delegateArea, seekBar)注意TouchDelegate要在父容器上设置且父容器布局变化后可能需要重新设置。4.3 进度条图片颜色不对多半是tint在捣乱Material主题下系统会自动给SeekBar的进度、背景和thumb加上tint颜色。这个tint会叠加在自定义图片上表现就是图片像蒙了一层雾或者整体变色甚至高光被压掉。遇到这种情况你可能会去代码里反复排查实际上就是主题默认tint在捣乱。布局中显式把三个tint置空就能解决android:progressTintnull android:progressBackgroundTintnull android:thumbTintnull这几个属性从API 21开始生效如果你项目minSdk小于21低版本不会受影响高版本显式置空即可。5. 实测踩坑记录这些坑我都替你踩过了5.1 splitTrack在thumb附近挖空进度条Android 5.0之后SeekBar默认会把thumb位置下面的一段进度轨道挖空防止滑块遮挡内容。这个设计在系统自带样式里还好但配自定义图片drawable时经常会在进度条中间出现一个莫名其妙的缺口尤其当进度刚好走到滑块下面时。解决方式就一行android:splitTrackfalse关闭之后进度图片会完整绘制不会被挖空。5.2 图片被拉伸变形普通png和.9.png怎么选如果轨道图是纯色或者简单渐变横向拉伸几乎看不出来普通png配合gravityfill_horizontal|center_vertical完全够用。但如果图片里包含圆角、高光、纹理或者文字横向拉伸必然变形。此时只有一条路用.9.png。把可拉伸区域限定在中间两端圆角和高光就固定不动了。我踩过的具体坑是把.9.png塞进bitmap标签里结果部分机型上拉伸规则不生效圆角直接变形。后面改成通过layer-list的item直接引用或者用layer-list包一层再放进clip问题才消失。5.3 ClipDrawable右端切掉圆角这个在素材章节提过实际做出来后的视觉冲击更明显。我第一版把进度图和背景图做成了完全一样的圆角结果进度走到50%时右端直角顶着背景圆角怎么看怎么别扭。后来把进度图右端改成了直角配合背景曲线的遮挡效果整体才自然。如果产品坚持进度条两端都必须始终圆角那只能在进度层之上再叠一个不随进度移动的圆角遮罩逻辑会复杂很多所以最好在切图阶段就沟通清楚。5.4 多个SeekBar复用同一Drawable导致串状态在RecyclerView里复用一个progressDrawable实例会导致两个SeekBar互相影响。因为Drawable内部维护了level状态A改了进度B的显示也跟着变。正确做法是每个SeekBar都通过Resources重新加载一份独立实例不要在代码里缓存同一个Drawable对象给多个控件使用。XML引用创建的Drawable每次膨胀都会生成新对象所以优先在XML中配置最省心。6. 从Demo到组件把图片SeekBar封装成通用控件6.1 attrs可配置化如果项目里多个页面都要用这类进度条建议直接封装成自定义View。在res/values/attrs.xml里定义好属性declare-styleable nameImageSeekBar attr nameseek_bgImage formatreference / attr nameseek_progressImage formatreference / attr nameseek_thumbImage formatreference / attr nameseek_trackHeight formatdimension / attr nameseek_thumbOffset formatdimension / /declare-styleable自定义View的构造函数里读取这些属性动态生成layer-list、ClipDrawable和thumb再设置给SeekBar。这样每个页面只需要几行属性就能配出不同风格的进度条不用每次重复写那一堆drawable配置。6.2 组件化之后还能做什么扩展把这张皮换好之后很多之前觉得难搞的需求就变得清晰了。比如播放器需要的两级进度缓冲进度和播放进度可以在同一个自定义View里画两层进度图一层用灰色表示缓冲一层用金色表示播放本质都是ClipDrawable的level控制。再比如拖动回调、时间联动也只是把SeekBar的OnSeekBarChangeListener封装成对外接口而已。最后分享一个小习惯跟Drawable样式相关的改动尽量在XML资源文件里完成代码里只做状态和逻辑控制。进度条这种样式反复调整的东西放在资源文件里UI改起来会快很多也避免每次都要重新编译整个类。本文还有配套的精品资源点击获取