目录
问题描述
原因分析
Android 6.x -10.x updateThumbAndTrackPos
Android 5.1 (API 22) updateThumbAndTrackPos
Android 4.1 (API 15) updateThumbPos
解决方案
工作需要对Seekbar的拖拽按钮设置一个自定义的icon,然后就遇到一个Seekbar拖拽按钮机型适配问题。
在Android 10等高版本系统机型上Seekbar的拖拽按钮展示正常,但是在比如Android5.1系统上展示异常。异常现象是拖拽按钮偏离了轨道中心位置,拖拽按钮顶部紧贴控件的顶部边界。
Seekbar拖拽按钮是个Drawable,它的位置由Drawable#setBounds设置。所以拖拽按钮异常意味着Drawable#getBounds的返回值有误。debug了一下bounds的值,发现错误的情况下,bounds的top值是0,而正确的情况下bounds的top值是一个计算出的数字,比如60px。从设置Seekbar拖拽按钮Thumb位置的代码,可以看出top值就是offset。而offset的值是从updateThumbAndTrackPos
中获取的。因此Thumb的top值取决于trackHeight和thumbHeight的大小关系。但是问题就出现在不同系统版本上计算top值的具体方法不一致,而且高低版本差异巨大。其实低版本是有bug的。
从Andorid 6.x版本开始,源码中对方法updateThumbAndTrackPos进行了彻底重构,算是解决了这个bug,因此6.x一直到现在的10.x这个方法的具体实现就没变化过。updateThumbAndTrackPos方法的实现简单直接,Thumb的top值取决于trackHeight和thumbHeight的大小关系,不论这两个值谁更大,都可以保证Thumb可以在竖直方向上中心点落到轨道上。从下面这段代码看,不会出现偏离轨道的情况,所以排除高版本的嫌疑,这也和测试结果相符合。
/**
* Updates the thumb drawable bounds.
*
* @param w Width of the view, including padding
* @param thumb Drawable used for the thumb
* @param scale Current progress between 0 and 1
* @param offset Vertical offset for centering. If set to
* {@link Integer#MIN_VALUE}, the current offset will be used.
*/
private void setThumbPos(int w, Drawable thumb, float scale, int offset) {
...
final int top, bottom;
...
top = offset;
bottom = offset + thumbHeight;
...
// Canvas will be translated, so 0,0 is where we start drawing
thumb.setBounds(left, top, right, bottom);
updateGestureExclusionRects();
}
private void updateThumbAndTrackPos(int w, int h) {
final int paddedHeight = h - mPaddingTop - mPaddingBottom;
final Drawable track = getCurrentDrawable();
final Drawable thumb = mThumb;
// The max height does not incorporate padding, whereas the height
// parameter does.
final int trackHeight = Math.min(mMaxHeight, paddedHeight);
final int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
// Apply offset to whichever item is taller.
final int trackOffset;
final int thumbOffset;
if (thumbHeight > trackHeight) {
final int offsetHeight = (paddedHeight - thumbHeight) / 2;
trackOffset = offsetHeight + (thumbHeight - trackHeight) / 2;
thumbOffset = offsetHeight;
} else {
final int offsetHeight = (paddedHeight - trackHeight) / 2;
trackOffset = offsetHeight;
thumbOffset = offsetHeight + (trackHeight - thumbHeight) / 2;
}
...
if (thumb != null) {
setThumbPos(w, thumb, getScale(), thumbOffset);
}
}
那兼容问题就出现在6.x之前的低版本上。6.x之前的版本,4.x和5.x每一个大版本该方法实现都会变化一次,也就是谷歌的工程师修改了多次才把这个bug改好。因此就存在4.x和5.x的兼容问题了。
对比一下发现API 22的源码updateThumbAndTrackPos方法的实现方式确实不一样,和高版本完全不同。当thumbHeight大于trackHeight的时候,很暴力的给top设置为0。这就是问题所在了。
/**
* Sets the thumb that will be drawn at the end of the progress meter within the SeekBar.
*
* If the thumb is a valid drawable (i.e. not null), half its width will be
* used as the new thumb offset (@see #setThumbOffset(int)).
*
* @param thumb Drawable representing the thumb
*/
public void setThumb(Drawable thumb) {
final boolean needUpdate;
if (mThumb != null && thumb != mThumb) {
mThumb.setCallback(null);
needUpdate = true;
} else {
needUpdate = false;
}
...
if (needUpdate) {
updateThumbAndTrackPos(getWidth(), getHeight());
...
}
}
private void updateThumbAndTrackPos(int w, int h) {
final Drawable track = getCurrentDrawable();
final Drawable thumb = mThumb;
// The max height does not incorporate padding, whereas the height
// parameter does.
final int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom);
final int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
// Apply offset to whichever item is taller.
final int trackOffset;
final int thumbOffset;
if (thumbHeight > trackHeight) {
trackOffset = (thumbHeight - trackHeight) / 2;
thumbOffset = 0;
} else {
trackOffset = 0;
thumbOffset = (trackHeight - thumbHeight) / 2;
}
...
if (thumb != null) {
setThumbPos(w, thumb, getScale(), thumbOffset);
}
}
再一次惊讶地发现每一个大版本的实现方式都不一样,不过好在4.x和5.x的行为是一致的。那要做的兼容就是把这个设置为0的情况给规避掉。
private void updateThumbPos(int w, int h) {
if (thumbHeight > trackHeight) {
if (thumb != null) {
setThumbPos(w, thumb, scale, 0);
}
...
} else {
...
int gap = (trackHeight - thumbHeight) / 2;
if (thumb != null) {
setThumbPos(w, thumb, scale, gap);
}
}
}
要解决这个问题,就要想办法在低版本系统上规避掉thumbHeight大于trackHeight的情况。之所以thumbHeight大于trackHeight,并不是thumbHeight的值太大了,而是trackHeight值太小了,远远小于getHeight的值。因为trackHeight的值是这样获取的:
final int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom);
这导致trackHeight的值取得是最小值,也就是mMaxHeight的值。我遇到的问题就是在5.1系统手机上mMaxHeight=48,
而h - mPaddingTop - mPaddingBottom = 180. 很显然如果trackHeight不应该是48,而应该是180.那只能想办法改变mMaxHeight的值。
mMaxHeight这个是ProgressBar类的成员变量,它的初始值是48.
private void initProgressBar() {
mMax = 100;
mProgress = 0;
mSecondaryProgress = 0;
mIndeterminate = false;
mOnlyIndeterminate = false;
mDuration = 4000;
mBehavior = AlphaAnimation.RESTART;
mMinWidth = 24;
mMaxWidth = 48;
mMinHeight = 24;
mMaxHeight = 48;
}
然后再初始化阶段会读取xml文件中的自定义值,如果设置了的话。
mMaxHeight = a.getDimensionPixelSize(R.styleable.ProgressBar_maxHeight, mMaxHeight);
到这里就找到了设置mMaxHeight的值的方法,就是在xml中给mMaxHeight指定一个比较大的值,从而规避这个问题。
android:maxHeight="180dip"
问题到这里就解决了。耗费很大精力分析这个问题,结果只修改一行代码就解决了,感觉这个过程颇有些戏剧性。
从这个兼容问题上可以看出来,在高版本系统上看着设计的合理的代码,由于低版本上源码的巨大差异,可能就不在适用。这是安卓平台常见的兼容问题。解决这类问题比较直接的办法就是把各个版本的代码都罗列出来进行比较。通过对比代码差异,就容易找到问题产生的原因了。