之前的项目里需要用到一个刻度尺型的选择控件,虽然项目的经历不是很愉快,但是控件既然写了就拿出来跟大家分享一下吧,希
望其中的一些解决思路能在大家以后遇到类似问题的时候提供一些启发。
先来看一下效果吧:
控件的三个难点:
1、画刻度
2、计算指针指向位置的刻度值
3、在滚动结束时如果指针不在刻度处,需要自动回调至最近的刻度
简单说一下解决思路:如果把每个刻度看作一个细长有颜色的View,难点1无非就是在Java代码中堆叠若干个布局,把这些View
排布上去,代码有点多,但是如果有在Java代码中添加布局的经验的话还是容易解决的;如果横向滑动效果用在外面套一个
HorizontalScrollView来实现,而HorizontalScrollView的滑动距离可以用getScrollX方法拿到,而刻度总长度和每一格刻度的
长度已知,指针指向位置的刻度值就可以计算了,难点2也ok了;难点3比较麻烦,需要在创建处HorizontalScrollView的时候重
写其中的一些方法来干预它的滚动和,然后在滚动结束时用HorizontalScrollView.smoothScrollTo()方法实现回滚,但是向左滚
还是向右滚、滚多少,又需要分都种情况来讨论。
说了这么多大家可能都有点绕晕了,直接上代码把,控件名字叫RulerView,只有一个文件。
RulerView.java:
public class RulerView extends RelativeLayout {
private Context context;
private HorizontalScrollView scrollView;
private LinearLayout parentLayout;
private RelativeLayout markLayout;
private ArrayList list;
// 左填充
private View leftView;
// 右填充
private View rightView;
private int width;
private int height;
private int initialPosition;
private int initialX;
// 滚动器
private Runnable scrollerTask;
// 指针颜色
private int markColor;
// 指针宽度
private int markWidth;
// 刻度颜色
private int normalColor;
// 刻度长度
private int normalLenght;
// 刻度宽度
private int normalWidth;
// 刻度字大小
private int textSize;
// 刻度字颜色
private int textColor;
// 刻度起点
private double start;
// 刻度终点
private double end;
// 刻度间隔
private int interval;
// 从起点到终点划分的格数
private int dividerNumber;
// 是否只允许指针停留在刻度处
private boolean isScaleOnly = true;
// 是否允许指针指向零刻度(第一个刻度)
private boolean isZeroAvailable = true;
// 刻度滚动时的回调
private OnScrollListener listener;
public RulerView(Context context) {
this(context, null);
}
public RulerView(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public RulerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
this.context = context;
markColor = Color.parseColor("#FF0000");
markWidth = dp2px(2);
normalColor = Color.parseColor("#000000");
normalLenght = dp2px(30);
normalWidth = dp2px(1);
textSize = 10;
textColor = Color.parseColor("#aaaaaa");
start = 0;
end = 100;
interval = dp2px(50);
dividerNumber = 10;
scrollerTask = new Runnable() {
public void run() {
int newX = scrollView.getScrollX();
// 如果不再滚动
if (initialX - newX == 0) {
final int remainder = initialX % interval;
// 如果指针停在刻度处
if (remainder == 0) {
// 如果设置此时停在零刻度,但设置又不允许,则回滚到下一刻度
if (!isZeroAvailable && scrollView.getScrollX() == 0) {
scrollView.smoothScrollTo(interval, 0);
if (listener != null) {
listener.onSelect(1, start + (end - start) / dividerNumber);
}
} else {
int position = initialX / interval;
if (listener != null) {
listener.onSelect(position, start + (end - start) * position / dividerNumber);
}
}
} else {
// 如果指针停留的位置超过刻度间距的一半,则滚至下一刻度
if (remainder > interval / 2) {
post(new Runnable() {
@Override
public void run() {
// 如果指针停在0和1刻度之间,但设置不允许停在零刻度,则滚至1
if (!isZeroAvailable && scrollView.getScrollX() < interval) {
scrollView.smoothScrollTo(interval, 0);
if (listener != null) {
listener.onSelect(1, start + (end - start) / dividerNumber);
}
// 否则滚至下一刻度
} else {
int position = (initialX - remainder) / interval + 1;
scrollView.smoothScrollTo(initialX - remainder + interval, 0);
if (listener != null) {
listener.onSelect(position, start + (end - start) * position / dividerNumber);
}
}
}
});
// 否则滚至上一刻度
} else {
post(new Runnable() {
@Override
public void run() {
// 如果见滚至0,但设置不允许,强制滚至1
if (!isZeroAvailable && scrollView.getScrollX() < interval) {
scrollView.smoothScrollTo(interval, 0);
if (listener != null) {
listener.onSelect(1, start + (end - start) / dividerNumber);
}
// 否则滚至上一刻度
} else {
int position = (initialX - remainder) / interval;
scrollView.smoothScrollTo(initialX - remainder, 0);
if (listener != null) {
listener.onSelect(position, start + (end - start) * position / dividerNumber);
}
}
}
});
}
}
} else {
initialX = scrollView.getScrollX();
scrollView.postDelayed(scrollerTask, 50);
}
}
};
/** 获取控件宽度
* 这里有一个疑惑,如果在onMeasure或onSizeChanged里执行此操作,高度和宽度数值正确,
* 但是scrollView的高宽都是0,getScrollX也是0,使滚动逻辑错乱,
* 纠结了很久,后来尝试在此回调中获取高宽和初始化,就没有这种问题了,很诡异
*/
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
getViewTreeObserver().removeGlobalOnLayoutListener(this);
width = getWidth();
height = getHeight();
initView();
}
});
}
private void initView() {
removeAllViews();
if (list == null) list = new ArrayList<>();
list.clear();
scrollView = new HorizontalScrollView(context) {
@Override
public void fling(int velocityX) {
super.fling(velocityX / 3);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (listener != null) {
listener.onScroll((double) l / interval * dividerNumber, isScaleOnly ? (start + (end - start) * getSelectPosition(l)
/ dividerNumber) : (end - start) * l / (interval * dividerNumber));
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
if (isScaleOnly) {
initialX = scrollView.getScrollX();
scrollView.postDelayed(scrollerTask, 50);
}
}
return super.onTouchEvent(ev);
}
};
scrollView.setHorizontalScrollBarEnabled(false);
parentLayout = new LinearLayout(context);
parentLayout.setOrientation(LinearLayout.HORIZONTAL);
parentLayout.setGravity(Gravity.BOTTOM);
// 添加左填充
leftView = new View(context);
parentLayout.addView(leftView, new LinearLayout
.LayoutParams(width / 2 - interval / 2, ViewGroup.LayoutParams.MATCH_PARENT));
// 画刻度
for (int i = 0; i < dividerNumber + 1; i++) {
LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setGravity(Gravity.CENTER_HORIZONTAL);
// 偶数刻度为长刻度,写上刻度值
if (i % 2 == 0) {
TextView textView = new TextView(context);
textView.setTextSize(textSize);
textView.setTextColor(textColor);
textView.setText((int) (start + (end - start) * i / dividerNumber) + "");
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams
(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
textParams.setMargins(0, 0, 0, dp2px(5));
layout.addView(textView, textParams);
}
// 画刻度线
View line = new View(context);
line.setBackgroundColor(normalColor);
LinearLayout.LayoutParams lineParams = new LinearLayout.LayoutParams(normalWidth, i % 2 == 0 ? normalLenght : normalLenght / 2);
layout.addView(line, lineParams);
parentLayout.addView(layout, new LinearLayout.LayoutParams
(interval, ViewGroup.LayoutParams.WRAP_CONTENT));
list.add(layout);
}
// 添加右填充
rightView = new View(context);
parentLayout.addView(rightView, new LinearLayout
.LayoutParams(width / 2 - interval / 2, ViewGroup.LayoutParams.MATCH_PARENT));
scrollView.addView(parentLayout, new FrameLayout.LayoutParams
(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
// 画指针
markLayout = new RelativeLayout(context);
View markView = new View(context);
markView.setBackgroundColor(markColor);
LayoutParams markParams = new LayoutParams
(markWidth, ViewGroup.LayoutParams.MATCH_PARENT);
markParams.addRule(RelativeLayout.CENTER_IN_PARENT);
markLayout.addView(markView, markParams);
addView(scrollView, new RelativeLayout.LayoutParams
(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
addView(markLayout, new RelativeLayout.LayoutParams
(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
setSeletion(initialPosition);
}
/**
* 重置方法
*/
public void reset() {
initView();
}
/**
* 让指针指向指定刻度
*
* @param position 刻度坐标
*/
public void setSeletion(final int position) {
if (!isZeroAvailable && position == 0) {
return;
}
if (scrollView != null) {
scrollView.post(new Runnable() {
@Override
public void run() {
scrollView.smoothScrollTo(position * interval, 0);
}
});
} else {
initialPosition = position;
}
}
/**
* 获取当前指针指向的刻度坐标
*
* @return
*/
public int getSelection() {
return getSelectPosition(scrollView.getScrollX());
}
/**
* 让指针指向指定总长度的百分比位置
*
* @param percentage 百分比
*/
public void setPercentage(final double percentage) {
if (percentage > 1) {
return;
}
post(new Runnable() {
@Override
public void run() {
scrollView.smoothScrollTo((int) (percentage * interval * dividerNumber), 0);
}
});
}
/**
* 获取当前指针指向位置在中场度中的百分比
*
* @return
*/
public double getPercentage() {
return (double) scrollView.getScrollX() / (interval * dividerNumber);
}
/**
* 获取总刻度数
*
* @return
*/
public int getScaleNumber() {
return dividerNumber + 1;
}
private int getSelectPosition(int x) {
int remainder = x % interval;
int divided = x / interval;
if (remainder == 0) {
return divided;
} else {
if (remainder >= interval / 2) {
return divided + 1;
} else {
return divided;
}
}
}
/**
* 滚动回调
*/
public interface OnScrollListener {
/**
* 滚动中的回调
*
* @param percentage 当前指针指向的位置占总长度的百分比
* @param current 当前指针指向的位置
*/
void onScroll(double percentage, double current);
/**
* 滚动停止时回调,只在isScaleOnly设置为true时有回调
*
* @param position 当前指针指向的刻度坐标
* @param current 当前指针指向的位置
*/
void onSelect(int position, double current);
}
/**
* 设置滚动回调
*
* @param listener
*/
public void setOnScrollListener(OnScrollListener listener) {
this.listener = listener;
}
/**
* 设置划分的总格数
*
* @param dividerNumber
*/
public void setDividerNumber(int dividerNumber) {
this.dividerNumber = dividerNumber;
}
/**
* 设置指针颜色
*
* @param markColor 颜色值
*/
public void setMarkColor(int markColor) {
this.markColor = markColor;
}
/**
* 设置指针宽度
*
* @param markWidth 宽度值,单位dp
*/
public void setMarkWidth(int markWidth) {
this.markWidth = dp2px(markWidth);
}
/**
* 设置刻度颜色
*
* @param normalColor 颜色值
*/
public void setNormalColor(int normalColor) {
this.normalColor = normalColor;
}
/**
* 设置刻度长度
*
* @param normalLenght 长度值,单位dp
*/
public void setNormalLenght(int normalLenght) {
this.normalLenght = dp2px(normalLenght);
}
/**
* 设置刻度宽度
*
* @param normalWidth 宽度值,单位dp
*/
public void setNormalWidth(int normalWidth) {
this.normalWidth = dp2px(normalWidth);
}
/**
* 设置刻度起点
*
* @param start
*/
public void setStart(double start) {
this.start = start;
}
/**
* 设置刻度终点
*
* @param end
*/
public void setEnd(double end) {
this.end = end;
}
/**
* 设置刻度间隔
*
* @param interval 间隔值,单位px
*/
public void setInterval(int interval) {
this.interval = interval;
}
/**
* 设置刻度文字大小
*
* @param textSize 大小值,单位sp
*/
public void setTextSize(int textSize) {
this.textSize = textSize;
}
/**
* 设置刻度文字颜色
*
* @param textColor 颜色值
*/
public void setTextColor(int textColor) {
this.textColor = textColor;
}
/**
* 设置是否只允许指针停在刻度处
*
* @param isScaleOnly
*/
public void setIsScaleOnly(boolean isScaleOnly) {
this.isScaleOnly = isScaleOnly;
}
/**
* 设置是允许指针停在零刻度处
*
* @param isZeroAvailable
*/
public void setIsZeroAvailable(boolean isZeroAvailable) {
this.isZeroAvailable = isZeroAvailable;
}
private int dp2px(float dipValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
}
然后就可以用了,上MainActivity.java和activity_main.xml:
MainActivity.java:
public class MainActivity extends Activity {
private TextView textView;
private RulerView rulerView;
private Button leftButton, rightButton;
private DecimalFormat format;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.textview);
rulerView = (RulerView) findViewById(R.id.rulerview);
leftButton = (Button) findViewById(R.id.left_button);
rightButton = (Button) findViewById(R.id.right_button);
format = new DecimalFormat("######0.00");
rulerView.setOnScrollListener(new RulerView.OnScrollListener() {
@Override
public void onScroll(double percentage, double current) {
textView.setText(format.format(current));
}
@Override
public void onSelect(int position, double current) {
textView.setText(format.format(current));
}
});
leftButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (rulerView.getSelection() != 0) {
rulerView.setSeletion(rulerView.getSelection() - 1);
}
}
});
rightButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (rulerView.getSelection() != rulerView.getScaleNumber() - 1) {
rulerView.setSeletion(rulerView.getSelection() + 1);
}
}
});
}
}
activity_main.xml:
用法很简单,各个参数都有默认设置,所以什么都不用设置也没问题。运行一下就能看到开头看到的效果了。
当然一般情况下根据不同的需求设置一些参数的,设置方法在源码注释里都写清楚了,大家可以自己试一试。这里简单的做个示范,添加代码:
rulerView.setDividerNumber(10);
rulerView.setStart(5000);
rulerView.setEnd(10000);
rulerView.setInterval(getWindowManager().getDefaultDisplay().getWidth() / 12);
rulerView.setMarkColor(Color.parseColor("#333333"));
rulerView.setMarkWidth(2);
rulerView.setNormalColor(Color.parseColor("#666666"));
rulerView.setNormalWidth(1);
rulerView.setNormalLenght(20);
rulerView.setTextSize(10);
rulerView.setSeletion(5);
rulerView.setTextColor(Color.parseColor("#888888"));
rulerView.setIsScaleOnly(false);
再次运行:
最后附上源码地址:点击打开链接
这次的内容就到这里,我们下次再见。