最近利用空闲时间,通过贝塞尔曲线写了一个Indicator,效果如下(视频转换gif效果不好)
完整代码传送门
对于原点的绘制及变化,需要对贝塞尔曲线及通过三阶贝塞尔曲线绘制圆形有初步了解,请参考以下文章
Bézier curve;Composite Bézier curve;Approximate a circle with cubic Bézier curves
对于贝塞尔曲线理论,没有较为扎实的数学基础还是很难理解的,笔者的高等数学早已把我抛弃了,很多符号都不记得,断断续续写了一个月才完成编写,主要是在分裂与粘合效果这碰了壁,很是汗颜~~~,好了直接上代码吧!
public class CustomIndicator extends View {
private static final double factor = 0.55191502449;
public static final int INDICATOR_TYPE_SCALE = 0;
public static final int INDICATOR_TYPE_GRADUAL = 1;
public static final int INDICATOR_TYPE_SPLIT = 2;
public static final int INDICATOR_TYPE_SCALE_AND_GRADUAL = 3;
private static final int DEFAULT_NORMAL_POINT_RADIUS = 8;
private static final int DEFAULT_SELECTED_POINT_RADIUS = 12;
private int heightMeasureSpec;
private Paint normalPointPaint;
private Paint selectedPointPaint;
private Paint targetPointPaint;
private float normalPointRadius;
private float selectedPointRadius;
private int pointInterval;
private int normalPointColor;
private int selectedPointColor;
private int indicatorType;
private int pointCount;
private List<PointF> relativeControlPoints;
// private int selectedPagePosition;
private int currentPagePosition;
private int targetPagePosition;
private int width;
private int height;
private Path arcPath;
private Path splitArcPath;
private float translationFactor;
private PagerAdapter adapter;
private DataSetObserver dataSetObserver;
private static final int SPLIT_OFFSET = DisplayUtil.dp2px(10);
private static final float SPLIT_RADIUS_FACTOR = 1.4f;
@IntDef({INDICATOR_TYPE_SCALE, INDICATOR_TYPE_GRADUAL, INDICATOR_TYPE_SCALE_AND_GRADUAL, INDICATOR_TYPE_SPLIT})
@Retention(RetentionPolicy.SOURCE)
private @interface IndicatorType {
}
public CustomIndicator(Context context) {
this(context, null);
}
public CustomIndicator(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomIndicator(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
private void init(AttributeSet attrs) {
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CustomIndicator);
indicatorType = typedArray.getInt(R.styleable.CustomIndicator_indicatorType, 0);
normalPointRadius = typedArray.getDimensionPixelSize(R.styleable.CustomIndicator_normalPointRadius, DEFAULT_NORMAL_POINT_RADIUS);
selectedPointRadius = typedArray.getDimensionPixelSize(R.styleable.CustomIndicator_selectedPointRadius, indicatorType == INDICATOR_TYPE_GRADUAL ? DEFAULT_NORMAL_POINT_RADIUS : DEFAULT_SELECTED_POINT_RADIUS);
pointInterval = typedArray.getDimensionPixelSize(R.styleable.CustomIndicator_pointInterval, 20);
normalPointColor = typedArray.getColor(R.styleable.CustomIndicator_normalPointColor, Color.parseColor("#FFFFFF"));
selectedPointColor = typedArray.getColor(R.styleable.CustomIndicator_selectedPointColor, Color.parseColor("#11EEEE"));
typedArray.recycle();
normalPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
normalPointPaint.setStyle(Paint.Style.FILL);
normalPointPaint.setColor(normalPointColor);
if (indicatorType == INDICATOR_TYPE_GRADUAL || indicatorType == INDICATOR_TYPE_SCALE_AND_GRADUAL) {
selectedPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
selectedPointPaint.setStyle(Paint.Style.FILL);
selectedPointPaint.setColor(selectedPointColor);
targetPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
targetPointPaint.setStyle(Paint.Style.FILL);
targetPointPaint.setColor(selectedPointColor);
} else if (indicatorType == INDICATOR_TYPE_SPLIT) {
if (selectedPointRadius < normalPointRadius * SPLIT_RADIUS_FACTOR) {
selectedPointRadius = (int) (normalPointRadius * SPLIT_RADIUS_FACTOR);
}
if (pointInterval < SPLIT_RADIUS_FACTOR * normalPointRadius * 2 + SPLIT_OFFSET) {
pointInterval = (int) (SPLIT_RADIUS_FACTOR * normalPointRadius * 2 + SPLIT_OFFSET);
}
}
arcPath = new Path();
splitArcPath = new Path();
relativeControlPoints = new ArrayList<>();
// 初始化绘制 1/4 圆弧的三阶贝塞尔曲线控制点相对坐标(相对圆心)
for (int i = 0; i < 8; i++) {
float x;
float y;
switch (i) {
case 0: { // 右下P0
x = normalPointRadius;
y = (float) (normalPointRadius * factor);
break;
}
case 1: { // 右下P1
x = (float) (normalPointRadius * factor);
y = normalPointRadius;
break;
}
case 2: { // 左下P2
x = -(float) (normalPointRadius * factor);
y = normalPointRadius;
break;
}
case 3: { // 左下P3
x = -normalPointRadius;
y = (float) (normalPointRadius * factor);
break;
}
case 4: { // 左上P4
x = -normalPointRadius;
y = -(float) (normalPointRadius * factor);
break;
}
case 5: { // 左上P5
x = -(float) (normalPointRadius * factor);
y = -normalPointRadius;
break;
}
case 6: { // 右上P6
x = (float) (normalPointRadius * factor);
y = -normalPointRadius;
break;
}
default: { // 右上P7
x = normalPointRadius;
y = -(float) (normalPointRadius * factor);
break;
}
}
PointF pointF = new PointF(x, y);
relativeControlPoints.add(pointF);
}
}
public void setIndicatorType(@IndicatorType int type) {
if (type == indicatorType) {
return;
}
indicatorType = type;
if (indicatorType == INDICATOR_TYPE_GRADUAL || indicatorType == INDICATOR_TYPE_SCALE_AND_GRADUAL) {
if (selectedPointPaint == null) {
selectedPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
selectedPointPaint.setStyle(Paint.Style.FILL);
selectedPointPaint.setColor(selectedPointColor);
}
if (targetPointPaint == null) {
targetPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
targetPointPaint.setStyle(Paint.Style.FILL);
targetPointPaint.setColor(selectedPointColor);
}
} else if (indicatorType == INDICATOR_TYPE_SPLIT) {
if (selectedPointRadius < normalPointRadius * SPLIT_RADIUS_FACTOR) {
selectedPointRadius = (int) (normalPointRadius * SPLIT_RADIUS_FACTOR);
}
if (pointInterval < SPLIT_RADIUS_FACTOR * normalPointRadius * 2 + SPLIT_OFFSET) {
pointInterval = (int) (SPLIT_RADIUS_FACTOR * normalPointRadius * 2 + SPLIT_OFFSET);
}
}
postInvalidate();
}
public void bindViewPager(final ViewPager viewPager) {
if (viewPager != null) {
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
LogUtil.w("posInfo: " + currentPagePosition + " " + position + " " + positionOffset + " " + positionOffsetPixels);
// 动态计算当前页与目标页位置
if (positionOffsetPixels > 0) {
if (position < currentPagePosition) {
translationFactor = 1 - positionOffset;
currentPagePosition = position + 1;
targetPagePosition = position;
} else {
translationFactor = positionOffset;
currentPagePosition = position;
targetPagePosition = position + 1;
}
} else {
translationFactor = positionOffset;
currentPagePosition = position;
targetPagePosition = position;
}
postInvalidate();
}
@Override
public void onPageSelected(int position) {
// selectedPagePosition = position;
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
adapter = viewPager.getAdapter();
if (adapter != null && adapter instanceof RealPagerAdapterImp) {
pointCount = ((RealPagerAdapterImp) adapter).getRealCount();
measure(0, heightMeasureSpec);
// 监听数据变化
dataSetObserver = new DataSetObserver() {
@Override
public void onChanged() {
pointCount = ((RealPagerAdapterImp) adapter).getRealCount();
if (currentPagePosition >= pointCount) {
currentPagePosition = pointCount - 1;
targetPagePosition = currentPagePosition;
}
postInvalidate();
}
};
adapter.registerDataSetObserver(dataSetObserver);
} else {
throw new RuntimeException("please set adapter before bind this viewPager");
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
this.heightMeasureSpec = heightMeasureSpec;
if (pointCount > 0) {
width = (pointCount - 1) * pointInterval + 2 + 2 * (int) selectedPointRadius;
} else {
width = 0;
}
if (heightMeasureSpec == MeasureSpec.EXACTLY) {
height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
} else {
height = (int) (selectedPointRadius * 2) + 2;
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
if (pointCount <= 0) {
LogUtil.e("adapter size is 0");
return;
}
float pointRadius;
if (height > 0 && width > 0) {
if (indicatorType == INDICATOR_TYPE_SPLIT) {
drawSplitTypeIndicator(canvas);
return;
}
float centerX;
float centerY = height / 2;
float endX;
float endY;
float centerXOffset = selectedPointRadius;
for (int i = 0; i < pointCount; i++) {
centerX = i * pointInterval + centerXOffset;
// 根据ViewPager滑动动态调整当前选中点和目标点半径
if (i == currentPagePosition) {
pointRadius = normalPointRadius + (1 - translationFactor) * (selectedPointRadius - normalPointRadius);
} else if (i == targetPagePosition) {
pointRadius = normalPointRadius + (translationFactor) * (selectedPointRadius - normalPointRadius);
} else {
pointRadius = normalPointRadius;
}
arcPath.reset();
arcPath.moveTo(centerX + pointRadius, centerY);
for (int k = 0; k < relativeControlPoints.size() / 2; k++) {
switch (k) {
case 0: { // 相对于圆心,第一象限
endX = centerX;
endY = centerY + pointRadius;
break;
}
case 1: { // 相对于圆心,第二象限
endX = centerX - pointRadius;
endY = centerY;
break;
}
case 2: { // 相对于圆心,第三象限
endX = centerX;
endY = centerY - pointRadius;
break;
}
default: { // 相对于圆心,第四象限
endX = centerX + pointRadius;
endY = centerY;
break;
}
}
float controlPointX1;
float controlPointY1;
float controlPointX2;
float controlPointY2;
if (i == currentPagePosition || i == targetPagePosition) {
// 控制点坐标根据ViewPager滑动做相应缩放
float stretchFactor = pointRadius / normalPointRadius;
controlPointX1 = centerX + relativeControlPoints.get(k * 2).x * stretchFactor;
controlPointY1 = centerY + relativeControlPoints.get(k * 2).y * stretchFactor;
controlPointX2 = centerX + relativeControlPoints.get(k * 2 + 1).x * stretchFactor;
controlPointY2 = centerY + relativeControlPoints.get(k * 2 + 1).y * stretchFactor;
} else {
controlPointX1 = centerX + relativeControlPoints.get(k * 2).x;
controlPointY1 = centerY + relativeControlPoints.get(k * 2).y;
controlPointX2 = centerX + relativeControlPoints.get(k * 2 + 1).x;
controlPointY2 = centerY + relativeControlPoints.get(k * 2 + 1).y;
}
arcPath.cubicTo(controlPointX1, controlPointY1, controlPointX2, controlPointY2, endX, endY);
if (indicatorType == INDICATOR_TYPE_GRADUAL || indicatorType == INDICATOR_TYPE_SCALE_AND_GRADUAL) {
int alpha = (int) (translationFactor * 255);
if (i == currentPagePosition) {
selectedPointPaint.setAlpha(255 - alpha);
normalPointPaint.setAlpha(alpha);
canvas.drawPath(arcPath, normalPointPaint);
canvas.drawPath(arcPath, selectedPointPaint);
} else if (i == targetPagePosition) {
targetPointPaint.setAlpha(alpha);
normalPointPaint.setAlpha(255 - alpha);
canvas.drawPath(arcPath, normalPointPaint);
canvas.drawPath(arcPath, targetPointPaint);
} else {
normalPointPaint.setAlpha(255);
canvas.drawPath(arcPath, normalPointPaint);
}
} else if (indicatorType == INDICATOR_TYPE_SCALE) {
canvas.drawPath(arcPath, normalPointPaint);
}
}
}
}
}
// 绘制分裂效果Indicator
private void drawSplitTypeIndicator(Canvas canvas) {
float centerX;
float centerY = height / 2;
float endX;
float endY;
float centerXOffset = selectedPointRadius;
float selectedSplitEndX = 0;
float selectedSplitEndY = 0;
// 控制分裂圆形半径的系数
float splitRadiusFactor;
if (translationFactor * pointInterval <= 2 * normalPointRadius) {
splitRadiusFactor = translationFactor * pointInterval / (normalPointRadius * 2);
splitRadiusFactor = (float) Math.log(1 + (Math.E - 1) * splitRadiusFactor);
} else if (translationFactor * pointInterval > pointInterval - 2 * normalPointRadius) {
splitRadiusFactor = (pointInterval - translationFactor * pointInterval) / (2 * normalPointRadius);
splitRadiusFactor = (float) Math.log((Math.E - 1) * splitRadiusFactor + 1);
} else {
splitRadiusFactor = 1;
}
// 动态调整分裂圆形的半径
float selectedSplitPointRadius = normalPointRadius + (1 - splitRadiusFactor) * (selectedPointRadius - normalPointRadius);
// 分裂圆形的滑动偏移量
float selectedSplitPointCenterXOffset = currentPagePosition < targetPagePosition ? translationFactor * (pointInterval) : -translationFactor * (pointInterval);
for (int i = 0; i < pointCount; i++) {
centerX = i * pointInterval + centerXOffset;
arcPath.reset();
arcPath.moveTo(centerX + normalPointRadius, centerY);
splitArcPath.reset();
if (i == currentPagePosition) {
float splitOffset = getSplitOffset();
//
if (currentPagePosition == targetPagePosition) {
splitArcPath.moveTo(centerX + selectedSplitPointCenterXOffset + selectedSplitPointRadius, centerY);
} else if (currentPagePosition > targetPagePosition) {
splitArcPath.moveTo(centerX + selectedSplitPointCenterXOffset + selectedSplitPointRadius + splitOffset, centerY);
} else {
float currentX = centerX + selectedSplitPointCenterXOffset + selectedSplitPointRadius;
// 根据粘合偏移量控制分裂圆形的起点(初始滑动分裂阶段为零,后半段粘合时有效)
splitArcPath.moveTo(currentX + getCurrentBondingOffset(currentX - centerX), centerY);
// 根据滑动分裂偏移量调整当前圆形的起点
arcPath.moveTo(centerX + normalPointRadius + splitOffset, centerY);
}
}
if (i == targetPagePosition && currentPagePosition > targetPagePosition) {
arcPath.moveTo(centerX + normalPointRadius + getTargetBondingOffset(), centerY);
}
for (int k = 0; k < relativeControlPoints.size() / 2; k++) {
switch (k) {
case 0: {
endX = centerX;
endY = centerY + normalPointRadius;
if (i == currentPagePosition) {
selectedSplitEndX = centerX + selectedSplitPointCenterXOffset;
selectedSplitEndY = centerY + selectedSplitPointRadius;
}
break;
}
case 1: {
endX = centerX - normalPointRadius;
endY = centerY;
if (i == currentPagePosition) {
selectedSplitEndX = centerX + selectedSplitPointCenterXOffset - selectedSplitPointRadius;
selectedSplitEndY = centerY;
if (currentPagePosition != targetPagePosition) {
float offset = getSplitOffset();
if (currentPagePosition > targetPagePosition) {
endX -= offset;
selectedSplitEndX -= getCurrentBondingOffset(centerX - selectedSplitEndX);
} else {
selectedSplitEndX -= offset;
}
}
}
if (i == targetPagePosition && currentPagePosition < targetPagePosition) {
endX -= getTargetBondingOffset();
}
break;
}
case 2: {
endX = centerX;
endY = centerY - normalPointRadius;
if (i == currentPagePosition) {
selectedSplitEndX = centerX + selectedSplitPointCenterXOffset;
selectedSplitEndY = centerY - selectedSplitPointRadius;
}
break;
}
default: {
endX = centerX + normalPointRadius;
endY = centerY;
if (i == currentPagePosition) {
selectedSplitEndX = centerX + selectedSplitPointCenterXOffset + selectedSplitPointRadius;
selectedSplitEndY = centerY;
if (currentPagePosition != targetPagePosition) {
float offset = getSplitOffset();
if (currentPagePosition < targetPagePosition) {
endX += offset;
selectedSplitEndX += getCurrentBondingOffset(selectedSplitEndX - centerX);
} else {
selectedSplitEndX += offset;
}
}
}
if (i == targetPagePosition && currentPagePosition > targetPagePosition) {
endX += getTargetBondingOffset();
}
break;
}
}
float controlPointX1 = centerX + relativeControlPoints.get(k * 2).x;
float controlPointY1 = centerY + relativeControlPoints.get(k * 2).y;
float controlPointX2 = centerX + relativeControlPoints.get(k * 2 + 1).x;
float controlPointY2 = centerY + relativeControlPoints.get(k * 2 + 1).y;
arcPath.cubicTo(controlPointX1, controlPointY1, controlPointX2, controlPointY2, endX, endY);
canvas.drawPath(arcPath, normalPointPaint);
if (i == currentPagePosition) {
float stretchFactor = selectedSplitPointRadius / normalPointRadius;
controlPointX1 = centerX + selectedSplitPointCenterXOffset + relativeControlPoints.get(k * 2).x * stretchFactor;
controlPointY1 = centerY + relativeControlPoints.get(k * 2).y * stretchFactor;
controlPointX2 = centerX + selectedSplitPointCenterXOffset + relativeControlPoints.get(k * 2 + 1).x * stretchFactor;
controlPointY2 = centerY + relativeControlPoints.get(k * 2 + 1).y * stretchFactor;
splitArcPath.cubicTo(controlPointX1, controlPointY1, controlPointX2, controlPointY2, selectedSplitEndX, selectedSplitEndY);
canvas.drawPath(splitArcPath, normalPointPaint);
}
}
}
}
// 动态调用该方法,获取分裂效果贝塞尔曲线偏移量
private float getSplitOffset() {
float participantX = translationFactor * pointInterval;
if (participantX > SPLIT_RADIUS_FACTOR * normalPointRadius * 2) {
participantX = 0;
}
float offset;
float offsetFactor;
offsetFactor = SPLIT_RADIUS_FACTOR - participantX / (2 * normalPointRadius);
offsetFactor = offsetFactor > 2 * (SPLIT_RADIUS_FACTOR - 1) ? 0 : offsetFactor;
offset = offsetFactor * participantX;
return offset;
}
// 动态调用该方法,获取分粘合果贝塞尔曲线偏移量
private float getTargetBondingOffset() {
float participantX = translationFactor * pointInterval - (pointInterval - SPLIT_RADIUS_FACTOR * normalPointRadius * 2);
if (participantX < 0) {
return 0;
}
float offset;
float offsetFactor;
offsetFactor = SPLIT_RADIUS_FACTOR - participantX / (2 * normalPointRadius);
offset = offsetFactor * participantX;
return offset;
}
private float getCurrentBondingOffset(float currentOffsetX) {
float participantX = translationFactor * pointInterval - (pointInterval - SPLIT_RADIUS_FACTOR * normalPointRadius * 2);
if (participantX < 0) {
return 0;
}
float offset;
float offsetFactor;
offsetFactor = SPLIT_RADIUS_FACTOR - participantX / (2 * normalPointRadius);
offset = offsetFactor * participantX;
if (offset + currentOffsetX > pointInterval + normalPointRadius) {
offset -= offset + currentOffsetX - pointInterval - normalPointRadius;
}
if (offset < 0) {
offset = 0;
}
return offset;
}
@Override
protected void onDetachedFromWindow() {
if (adapter != null && dataSetObserver != null) {
adapter.unregisterDataSetObserver(dataSetObserver);
}
super.onDetachedFromWindow();
}
}
本示例的圆形绘制,是从右下方1/4圆弧开始(相对于圆心的第一象限),顺时针依次绘制。绘制三阶贝塞尔曲线,需要两个控制点,一个起点与一个终点,绘制圆形需要依次绘制四个三阶贝塞尔曲线,即需要八个控制点,而示例代码中的relativeControlPoints列表存储了绘制圆形所需要的八个控制点相对于圆心的坐标。对于绘制圆形所需的控制点坐标的由来,强烈建议您参考这篇文章:Approximate a circle with cubic Bézier curves,并注意数学上坐标系与Android坐标系的差别,示意图如下(由于制图工具限制,会有偏差,仅做示意):
对于圆形大小的控制,其重点在于根据滑动偏移系数动态调整半径大小及控制点坐标的大小,二者做相同比例缩放即可;对于分裂与粘合效果,其重点在于根据滑动偏移系数分别动态调整当前圆形、目标圆形及分裂圆形的对应圆弧的起点与终点的偏移量。示例代码中有相关注释,细节部分请参考代码。