前言
这是新开的博客第一篇文章。这一篇针对的是自定义控件。在github上有一个自定义控件的效果如下:
这个水平方向上无限滚动的控件,可以用来制作自定义进度条,或者一些tab效果。
具体使用方法请移步github。
它实现该效果只有50行代码不到,所以写这篇博客来记录该控件的实现过程。
1. 分析需求
- 设置进去的图片可以水平滚动;
- 滚动展示的图片可以自行设置,滚动的速度也可以自行设置,;
- 速度为正,向后滚动(从右向左滚动);为负时,向前滚动(从左向右滚动);
2. xml资源相关
能够自行设置,那么需要去设置自定义属性来控制速度及滚动展示的图片。
在res/values下创建attrs.xml,并采用以下方式定义自定义属性:
<resource>
<declare-styleable name="ScrollingView">
<attr name="speed" format="dimension" />
<attr name="src" format="reference" />
</declare-styleable>
</resource>
speed为速度,src为滚动展示的图片资源。
在layout布局文件中使用自定义属性,首先在根布局view中设置命名空间:
xmlns:app="http://schemas.android.com/apk/res-auto"
然后在自定义控件中设置自定义属性
3. java代码
3.1 初始化
创建一个View类,并在构造方法中获得自定义属性speed和图片。凡是在xml中定义的控件,会调用以下形式的构造方法:
public ScrollingView(Context context, AttributeSet attrs) {
super(context, attrs);
try{
speed = typedArray.getDimensionPixelSize(R.styleable.ScrollingView_speed, 1);
bitmap = BitmapFactory.decodeResource(getResources(), typedArray.getResourceId(R.styleable.ScrollingView_src, 0));
}finally {
typedArray.recycle();
}
}
3.2 onMeasure测量
需要设置控件的宽高,不然会出现高度显示不正常。这里采用的方式为:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), bitmap.getHeight());
}
3.3 onDraw绘制
关键点来了,该控件最大的难点就在绘制,我将绘制逻辑分为三个要点:
3.3.1 要点一
如果当前的控件宽度大于要滚动的图片宽度,那么会出现空白,应该需要重复绘制多个图片来填充。
- 解决方案:用一个变量left记录当前canvas绘制bitmap时的左方基坐标,如果left小于控件的宽度,就继续绘制,每绘制一次图片,就让left的值递增,每次递增的值为bitmap的宽度,这种方式可以实现从左往右的绘制图片出来填充满控件。
具体代码如下:
protected void onDraw(Canvas canvas) {
//获取要滚动的图片的宽度
float layerWidth = bitmap.getWidth();
//使用一个变量来作为绘制bitmap时的左边基坐标
float left = 0;
//如果left不大于控件的宽度,则循环绘制
while (left < getMeasuredWidth()) {
canvas.drawBitmap(bitmap, left, 0, null);
left += layerWidth;
}
}
3.3.2 要点二
根据speed属性,来设置图片往后退的速度。
- 解决方案:通过不断修改canvas.drawBitmap()中的left,根据speed,不断的让left坐标变小,并调用重绘方法,从而使得图片不断后退。关于left变小的偏移量,使用全局变量offset进行记录。
具体代码如下:
private float offset;
@Override
protected void onDraw(Canvas canvas) {
//获取要滚动的图片的宽度
float layerWidth = bitmap.getWidth();
//使用一个变量来作为绘制bitmap的左边基坐标
float left = offset;
//如果left不大于控件的宽度,则循环绘制
while (left < getMeasuredWidth()) {
canvas.drawBitmap(bitmap, left, 0, null);
left += layerWidth;
}
//全局变量offser用来记录left的偏移量
offset = offset-speed;
postInvalidate();
}
这样写导致left的偏移量越来越小,代码会在手机屏幕左方看不见的地方疯狂绘制,这显然不太效率。这里多加一个判断,如果offset超过一定的界限,就重置。
protected void onDraw(Canvas canvas) {
//获取要滚动的图片的宽度
float layerWidth = bitmap.getWidth();
if (offset < -layerWidth) {
offset += (floor(abs(offset) / layerWidth) * layerWidth);
//offset = 0;
}
...
}
3.3.3 要点三
如果速度设置为负数,图片应该不再后退,而是不断前进。
- 解决方案:并让左方基坐标不断变大,并改变绘制的方向,改为从右往左的方向绘制图片,就可以让图片变成向前滚了。
- 如果需要从右向左,那么开始绘制第一个图片时,左方的基坐标不再是从0开始,而是:控件的宽度-图片bitmap的宽度。
- 在当前的情况下,为了left变量总体趋势不断变小,同时能够不断的让左方基坐标变大,所以在循环当中,左方基坐标改为getMeasureWidth()-bitmap.getWidth()-left。
- left变量总体趋势不断变小,offset也应该不断变小,而当前speed为负,所以在对offset偏移量的减去speed操作时,对speed采用绝对值abs。
protected void onDraw(Canvas canvas) {
//获取要滚动的图片的宽度
float layerWidth = bitmap.getWidth();
...
//如果left不大于控件的宽度,则循环绘制
while (left < getMeasuredWidth()) {
canvas.drawBitmap(bitmap, getBitmapLeft(layerWidth, left), 0, null);
left += layerWidth;
}
//全局变量offser用来记录left的偏移量
if(isStarted){
offset = offset-abs(speed);
postInvalidate();
}
}
/**
* @param layerWidth bitmap图片的宽度
* @return
*/
private float getBitmapLeft(float layerWidth, float left) {
if (speed < 0) {
return getMeasuredWidth() - layerWidth - left;
} else {
return left;
}
}
3.4 功能完善
为了能够让开发者自由控制图片滚动,项目中还加了一个boolean值用来控制。并提供对应的公有方法。
具体代码如下:
private boolean isStarted = false;
public ScrollingView(Context context, AttributeSet attrs) {
...
start();
}
/**Start the animation*/
public void start() {
if (!isStarted) {
isStarted = true;
postInvalidate();
}
}
protected void onDraw(Canvas canvas) {
...
//全局变量offser用来记录left的偏移量
if(isStarted){
offset = offset-speed;
postInvalidate();
}
}
/**Stop the animation*/
public void stop() {
if (isStarted) {
isStarted = false;
invalidate();
}
}
public boolean isStarted(){
return isStarted;
}
结束语
有兴趣的小伙伴可以参考这个思路,通过修改drawBitmap方法中top基坐标,实现一下Image在竖直方向上的无限滚动。