最近公司的项目中需要一个游标效果控件,可以滑动游标选择相应的时间域。设计效果图如下:
于是乎,决定自己动手实现一下这个效果,也算是复习自定义view相关知识。
首先,自定义属性:attr.xml
<declare-styleable name="CursorView">
<attr name="current_bg_color" format="color|reference"></attr>
<attr name="current_text_color" format="color|reference"></attr>
<attr name="incurrent_text_color" format="color|reference"></attr>
<attr name="textsize" format="dimension"></attr>
<attr name="stroke_width" format="dimension"></attr>
<attr name="stroke_color" format="color|reference"></attr>
</declare-styleable>
自定义View类如下:
package com.baicells.omcserver.view;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import com.baicells.omcserver.R;
import java.util.ArrayList;
public class CursorView extends View {
private int width;
private int height;
private int radius;
private int strokeWidth;
private int curTextColor;
private int inCurTextColor;
private ArrayList<String> cursorText;
private Paint borderPaint;
private Paint innerBgPaint;
private Paint textPaint;
private int current;
private float offsetX;
private RectF currentRectF;
private boolean isInCurrent;
public CursorView(Context context) {
super(context);
}
public CursorView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
borderPaint = new Paint();
borderPaint.setAntiAlias(true);
borderPaint.setStyle(Paint.Style.FILL.STROKE);
innerBgPaint = new Paint();
innerBgPaint.setAntiAlias(true);
innerBgPaint.setStyle(Paint.Style.FILL_AND_STROKE);
textPaint = new Paint();
textPaint.setAntiAlias(true);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CursorView);
int indexCount = typedArray.getIndexCount();
for (int i = 0; i < indexCount; i++) {
int index = typedArray.getIndex(i);
switch (index) {
case R.styleable.CursorView_current_bg_color:
innerBgPaint.setColor(typedArray.getColor(index, Color.GREEN));
break;
case R.styleable.CursorView_current_text_color:
curTextColor = typedArray.getColor(index, Color.WHITE);
break;
case R.styleable.CursorView_incurrent_text_color:
inCurTextColor = typedArray.getColor(index, Color.BLACK);
break;
case R.styleable.CursorView_stroke_color:
borderPaint.setColor(typedArray.getColor(index, Color.RED));
break;
case R.styleable.CursorView_textsize:
textPaint.setTextSize(typedArray.getDimensionPixelSize(index, 20));
break;
case R.styleable.CursorView_stroke_width:
strokeWidth = typedArray.getDimensionPixelSize(index, 3);
break;
}
}
typedArray.recycle();
borderPaint.setStrokeWidth(strokeWidth);
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
public CursorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setCursorText(ArrayList<String> cursorText) {
this.cursorText = cursorText;
invalidate();
}
public void setCurrent(int current) {
this.current = current;
invalidate();
}
public int getCurrent() {
return current;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
width = 300;
height = 20;
if (modeWidth == MeasureSpec.EXACTLY) {
width = sizeWidth;
}
if (modeHeight == MeasureSpec.EXACTLY) {
height = sizeHeight;
}
setMeasuredDimension(width, height);
radius = height / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画整个大背景
RectF rect = new RectF(strokeWidth / 2, strokeWidth / 2, width - strokeWidth / 2, height - strokeWidth / 2);
canvas.drawRoundRect(rect, radius, radius, borderPaint);
if (cursorText != null) {
int size = cursorText.size();
float everyWidth = width * 1.0f / size;
//画当前选中时间维度的背景
RectF currBgRectF = getCurrBgRectF(everyWidth);
currentRectF = currBgRectF;
canvas.drawRoundRect(currBgRectF, radius, radius, innerBgPaint);
//循环画时间维度文本
for (int i = 0; i < size; i++) {
textPaint.setShader(null);
String text = cursorText.get(i);
Paint.FontMetricsInt metricsInt = textPaint.getFontMetricsInt();
float textwidth = textPaint.measureText(text);
float preblank = (everyWidth - textwidth) / 2.0f;
float x = everyWidth * i + preblank;
float y = (height - metricsInt.bottom + metricsInt.top) / 2.0f - metricsInt.top;
if (currBgRectF.left >= x && currBgRectF.left <= x + textwidth) {
float split = (currBgRectF.left - x) / textwidth;
LinearGradient shader = new LinearGradient(x, 0, x + textwidth,
0, new int[]{inCurTextColor, curTextColor}, new float[]{
split, split + 0.01f}, Shader.TileMode.CLAMP);
textPaint.setShader(shader);
}
if (currBgRectF.right >= x && currBgRectF.right <= x + textwidth) {
float split = (currBgRectF.right - x) / textwidth;
LinearGradient shader = new LinearGradient(x, 0, x + textwidth,
0, new int[]{curTextColor, inCurTextColor}, new float[]{
split, split + 0.01f}, Shader.TileMode.CLAMP);
textPaint.setShader(shader);
}
if (currBgRectF.left < x && currBgRectF.right > x + textwidth) {
textPaint.setColor(curTextColor);
} else {
textPaint.setColor(inCurTextColor);
}
canvas.drawText(text, x, y, textPaint);
}
}
}
private RectF getCurrBgRectF(float everyWidth) {
float left = everyWidth * current + offsetX;
float right = everyWidth * (current + 1) + offsetX;
if (left < 0) {
left = 0;
right = everyWidth;
}
if (right > width) {
left = width - everyWidth;
right = width;
}
return new RectF(left, 0, right, height);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
ViewParent parent = null;
while (true) {
if (parent != null && parent instanceof ViewPager) {
break;
}
if (parent != null) {
parent = parent.getParent();
} else {
parent = getParent();
}
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
parent.requestDisallowInterceptTouchEvent(true);
break;
default:
parent.requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
private float downX = 0;
private float downY = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
isInCurrent = currentRectF.contains(downX, downY);
break;
case MotionEvent.ACTION_MOVE:
if (isInCurrent) {
offsetX = x - downX;
invalidate();
}
break;
case MotionEvent.ACTION_UP:
if (isInCurrent) {
final NextCurrent nextCurrent = computeScrollX(currentRectF.left);
final float lastX = offsetX;
ValueAnimator anim = ValueAnimator.ofFloat(0, nextCurrent.getX());
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
offsetX = lastX + value;
invalidate();
float fraction = animation.getAnimatedFraction();
if (fraction == 1.0f) {
current = nextCurrent.getIndex();
offsetX = 0;
invalidate();
}
}
});
anim.setDuration(100);
anim.start();
}
break;
}
return true;
}
private NextCurrent computeScrollX(float left) {
NextCurrent next = new NextCurrent();
int index = 0;
if (cursorText != null) {
int size = cursorText.size();
float everyWidth = width * 1.0f / size;
float min = everyWidth + 1;
for (int i = 0; i < size; i++) {
float abs = Math.abs(left - everyWidth * i);
if (abs < min) {
min = abs;
index = i;
}
}
if (everyWidth * index > left) {
next.setX(min);
} else {
next.setX(-min);
}
next.setIndex(index);
}
return next;
}
private class NextCurrent {
private int index;
private float x;
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
}
}
本控件支持的特性:
1、上述自定义View代码涉及到了手势滑动相关处理,当前选中的时间域背景跟随手指滑动进行移动,当手指抬起时,计算当前位置,并选择最近的时间域进行回弹(类似ViewPager中page页回弹效果)。
2、手指拖动当前时间域进行滑动过程中,对于不同时间域的文本内容,当内容与时间域背景有重叠时,支持颜色变化。
3、由于我的项目中自定义View是用在ViewPager下,所以有处理滑动冲突,当手指触摸位置为自定义View时请求父View不拦截事件,由当前自定义View处理该事件。
当前page页面对应的xml布局文件中的代码部分:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.baicells.omcserver.view.CursorView
android:id="@+id/cursor"
android:layout_width="328dp"
android:layout_height="28dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
app:current_bg_color="@android:color/holo_green_light"
app:current_text_color="@android:color/white"
app:incurrent_text_color="@android:color/black"
app:stroke_color="@android:color/holo_green_light"
app:stroke_width="2dp"
app:textsize="14sp" />
</LinearLayout>
运行效果截图:
小视频: