超级无敌SeekBar专门为笨B打造
```
package com.example.myapplicationwithg;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import androidx.annotation.Nullable;
public class CustomSeekBar extends View {
private static final float SCALE_FACTOR = 1.5f;
private static final long RELEASE_DELAY_MS = 200L;
public static final int ORIENTATION_HORIZONTAL = 0;
public static final int ORIENTATION_VERTICAL = 1;
private int trackColor = Color.LTGRAY;
private Drawable progressDrawable;
private int progressColorPressed = Color.BLUE;
private Drawable edgeDrawable;
private int edgeColorPressed = Color.BLACK;
private int thumbColor = Color.WHITE;
private Drawable thumbDrawable = null;
private float userSetTrackHeight = -1f;
private float baseTrackHeight = 0f;
private float pressedTrackHeight = 0f;
private float edgeWidth = 0f;
private float paddingAroundThumb = 0f;
private float trackProgressGap = 0f;
private int orientation = ORIENTATION_HORIZONTAL;
private int progress = 5;
private int maxProgress = 20;
private boolean isPressedState = false;
private float currentTrackHeight = 0f;
private int currentThumbAlpha = 0;
private float currentThumbRadius = 0f;
private ValueAnimator animator = null;
private final RectF trackRect = new RectF();
private final RectF progressRect = new RectF();
private final Rect thumbBounds = new Rect();
private final Path clipPath = new Path();
private float thumbCenterX = 0f;
private float thumbCenterY = 0f;
private final Paint commonPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Runnable releaseRunnable = () -> setPressedState(false);
private float touchExtension;
public interface OnProgressChangeListener {
void onProgressChanged(int progress);
void onStopTrackingTouch(int progress);
}
private OnProgressChangeListener onProgressChangeListener = null;
public void setOnProgressChangeListener(OnProgressChangeListener listener) {
this.onProgressChangeListener = listener;
}
public CustomSeekBar(Context context) {
this(context, null);
}
public CustomSeekBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
float density = getResources().getDisplayMetrics().density;
touchExtension = 20f * density;
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomSeekBar);
try {
trackColor = a.getColor(R.styleable.CustomSeekBar_seekbar_trackColor, Color.LTGRAY);
progressDrawable = a.getDrawable(R.styleable.CustomSeekBar_seekbar_progressDrawable);
progressColorPressed = a.getColor(R.styleable.CustomSeekBar_seekbar_progressColorPressed, Color.BLUE);
edgeDrawable = a.getDrawable(R.styleable.CustomSeekBar_seekbar_edgeDrawable);
edgeColorPressed = a.getColor(R.styleable.CustomSeekBar_seekbar_edgeColorPressed, Color.BLACK);
thumbColor = a.getColor(R.styleable.CustomSeekBar_seekbar_thumbColor, Color.WHITE);
thumbDrawable = a.getDrawable(R.styleable.CustomSeekBar_seekbar_thumbDrawable);
userSetTrackHeight = a.getDimension(R.styleable.CustomSeekBar_seekbar_trackHeight, -1f);
edgeWidth = a.getDimension(R.styleable.CustomSeekBar_seekbar_edgeWidth, 2f * density);
paddingAroundThumb = a.getDimension(R.styleable.CustomSeekBar_seekbar_thumbPadding, 2f * density);
trackProgressGap = a.getDimension(R.styleable.CustomSeekBar_seekbar_progressPadding, 5f * density);
maxProgress = a.getInt(R.styleable.CustomSeekBar_seekbar_max, 100);
progress = a.getInt(R.styleable.CustomSeekBar_seekbar_progress, 50);
orientation = a.getInt(R.styleable.CustomSeekBar_seekbar_orientation, ORIENTATION_HORIZONTAL);
} finally {
a.recycle();
}
}
setClickable(true);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w <= 0 || h <= 0) return;
float availableSize = (orientation == ORIENTATION_HORIZONTAL) ?
(h - getPaddingTop() - getPaddingBottom()) :
(w - getPaddingLeft() - getPaddingRight());
if (userSetTrackHeight >= 0) {
baseTrackHeight = userSetTrackHeight;
pressedTrackHeight = Math.min(baseTrackHeight * SCALE_FACTOR, availableSize);
} else {
pressedTrackHeight = availableSize;
baseTrackHeight = availableSize / SCALE_FACTOR;
}
currentTrackHeight = isPressedState ? pressedTrackHeight : baseTrackHeight;
updateThumbRadius();
updateGeometry();
}
private void updateThumbRadius() {
float pThickness = Math.max(0f, currentTrackHeight - 2 * trackProgressGap - edgeWidth);
currentThumbRadius = Math.max(0f, pThickness / 2f - paddingAroundThumb);
}
private float getFixedSafeMargin() {
float maxPThickness = Math.max(0f, pressedTrackHeight - 2 * trackProgressGap - edgeWidth);
float maxRadius = Math.max(0f, maxPThickness / 2f - paddingAroundThumb);
return trackProgressGap + edgeWidth / 2f + paddingAroundThumb + maxRadius;
}
private void updateGeometry() {
float ratio = (float) progress / maxProgress;
float safeMargin = getFixedSafeMargin();
float innerEdge = trackProgressGap + edgeWidth / 2f;
if (orientation == ORIENTATION_HORIZONTAL) {
float trackW = getWidth() - getPaddingLeft() - getPaddingRight();
float tTop = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom() - currentTrackHeight) / 2f;
trackRect.set(getPaddingLeft(), tTop, getPaddingLeft() + trackW, tTop + currentTrackHeight);
float minX = trackRect.left + safeMargin, maxX = trackRect.right - safeMargin;
thumbCenterX = minX + (maxX - minX) * ratio;
thumbCenterY = trackRect.centerY();
float pHeight = Math.max(0f, currentTrackHeight - 2 * trackProgressGap - edgeWidth);
float pTop = trackRect.top + (currentTrackHeight - pHeight) / 2f;
float wrap = isPressedState ? (currentThumbRadius + paddingAroundThumb) : 0f;
float right = (progress == maxProgress) ? trackRect.right - innerEdge : clamp(thumbCenterX + wrap, trackRect.left + innerEdge, trackRect.right - innerEdge);
progressRect.set(trackRect.left + innerEdge, pTop, right, pTop + pHeight);
} else {
float trackH = getHeight() - getPaddingTop() - getPaddingBottom();
float tLeft = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight() - currentTrackHeight) / 2f;
trackRect.set(tLeft, getPaddingTop(), tLeft + currentTrackHeight, getPaddingTop() + trackH);
float minY = trackRect.top + safeMargin, maxY = trackRect.bottom - safeMargin;
thumbCenterY = minY + (maxY - minY) * ratio;
thumbCenterX = trackRect.centerX();
float pWidth = Math.max(0f, currentTrackHeight - 2 * trackProgressGap - edgeWidth);
float pLeft = trackRect.left + (currentTrackHeight - pWidth) / 2f;
float wrap = isPressedState ? (currentThumbRadius + paddingAroundThumb) : 0f;
float bottom = (progress == maxProgress) ? trackRect.bottom - innerEdge : clamp(thumbCenterY + wrap, trackRect.top + innerEdge, trackRect.bottom - innerEdge);
progressRect.set(pLeft, trackRect.top + innerEdge, pLeft + pWidth, bottom);
}
}
@Override
protected void onDraw(Canvas canvas) {
float corner = currentTrackHeight / 2f;
// 1. 轨道背景
commonPaint.setStyle(Paint.Style.FILL);
commonPaint.setColor(trackColor);
commonPaint.setAlpha(255);
canvas.drawRoundRect(trackRect, corner, corner, commonPaint);
// 2. 进度条绘制
if (!progressRect.isEmpty() && (progress > 0 || isPressedState)) {
float pCorner = (orientation == ORIENTATION_HORIZONTAL) ? progressRect.height() / 2f : progressRect.width() / 2f;
if (isPressedState) {
// 按压模式修复:分两次绘制,先填色再描边
commonPaint.setStyle(Paint.Style.FILL);
commonPaint.setColor(progressColorPressed);
commonPaint.setAlpha(255);
canvas.drawRoundRect(progressRect, pCorner, pCorner, commonPaint);
if (edgeWidth > 0) {
commonPaint.setStyle(Paint.Style.STROKE);
commonPaint.setStrokeWidth(edgeWidth);
commonPaint.setColor(edgeColorPressed);
canvas.drawRoundRect(progressRect, pCorner, pCorner, commonPaint);
}
// 还原状态
commonPaint.setStyle(Paint.Style.FILL);
} else if (progressDrawable != null) {
// 默认渐变模式
canvas.save();
clipPath.reset();
clipPath.addRoundRect(progressRect, pCorner, pCorner, Path.Direction.CW);
canvas.clipPath(clipPath);
progressDrawable.setBounds((int) progressRect.left, (int) progressRect.top, (int) progressRect.right, (int) progressRect.bottom);
progressDrawable.draw(canvas);
if (edgeDrawable != null && edgeWidth > 0) {
edgeDrawable.setBounds((int) progressRect.left, (int) progressRect.top, (int) progressRect.right, (int) progressRect.bottom);
edgeDrawable.draw(canvas);
}
canvas.restore();
}
}
// 3. 滑块绘制
if (currentThumbAlpha > 0 && currentThumbRadius > 0) {
commonPaint.setStyle(Paint.Style.FILL);
if (thumbDrawable != null) {
thumbBounds.set((int) (thumbCenterX - currentThumbRadius), (int) (thumbCenterY - currentThumbRadius),
(int) (thumbCenterX + currentThumbRadius), (int) (thumbCenterY + currentThumbRadius));
thumbDrawable.setBounds(thumbBounds);
thumbDrawable.setAlpha(currentThumbAlpha);
thumbDrawable.draw(canvas);
} else {
commonPaint.setColor(thumbColor);
commonPaint.setAlpha(currentThumbAlpha);
canvas.drawCircle(thumbCenterX, thumbCenterY, currentThumbRadius, commonPaint);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isTouchInArea(event.getX(), event.getY())) {
removeCallbacks(releaseRunnable);
setPressedState(true);
updateProgressFromTouch(event.getX(), event.getY());
if (getParent() != null) getParent().requestDisallowInterceptTouchEvent(true);
return true;
}
break;
case MotionEvent.ACTION_MOVE:
if (isPressedState) updateProgressFromTouch(event.getX(), event.getY());
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (isPressedState) {
if (onProgressChangeListener != null)
onProgressChangeListener.onStopTrackingTouch(progress);
postDelayed(releaseRunnable, RELEASE_DELAY_MS);
if (getParent() != null) getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
return super.onTouchEvent(event);
}
private void updateProgressFromTouch(float tx, float ty) {
float safeMargin = getFixedSafeMargin();
float minPos = (orientation == ORIENTATION_HORIZONTAL) ? trackRect.left + safeMargin : trackRect.top + safeMargin;
float maxPos = (orientation == ORIENTATION_HORIZONTAL) ? trackRect.right - safeMargin : trackRect.bottom - safeMargin;
float current = (orientation == ORIENTATION_HORIZONTAL) ? tx : ty;
float ratio = (clamp(current, minPos, maxPos) - minPos) / Math.max(1f, maxPos - minPos);
int newProgress = Math.round(ratio * maxProgress);
if (newProgress != progress) {
progress = clamp(newProgress, 0, maxProgress);
updateGeometry();
invalidate();
performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
if (onProgressChangeListener != null)
onProgressChangeListener.onProgressChanged(progress);
}
}
private void setPressedState(boolean pressed) {
if (this.isPressedState == pressed) return;
this.isPressedState = pressed;
if (animator != null) animator.cancel();
final float startH = currentTrackHeight, targetH = pressed ? pressedTrackHeight : baseTrackHeight;
final int startA = currentThumbAlpha, targetA = pressed ? 255 : 0;
animator = ValueAnimator.ofFloat(0f, 1f).setDuration(pressed ? 200 : 150);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addUpdateListener(animation -> {
float f = animation.getAnimatedFraction();
currentTrackHeight = startH + (targetH - startH) * f;
currentThumbAlpha = isPressedState ? (int) (startA + (targetA - startA) * f) : 0;
updateThumbRadius();
updateGeometry();
invalidate();
});
animator.start();
}
private boolean isTouchInArea(float x, float y) {
return x >= (trackRect.left - touchExtension) && x <= (trackRect.right + touchExtension) &&
y >= (trackRect.top - touchExtension) && y <= (trackRect.bottom + touchExtension);
}
public void setProgress(int v) {
this.progress = clamp(v, 0, maxProgress);
updateGeometry();
invalidate();
}
private float clamp(float val, float min, float max) {
return Math.max(min, Math.min(max, val));
}
private int clamp(int val, int min, int max) {
return Math.max(min, Math.min(max, val));
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (animator != null) animator.cancel();
removeCallbacks(releaseRunnable);
}
}
```
```
```
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomSeekBar">
<attr name="seekbar_trackColor" format="color" /><!-- 轨道背景颜色 -->
<attr name="seekbar_progressDrawable" format="reference|color" /><!-- 进度条默认状态图片 -->
<attr name="seekbar_progressColorPressed" format="color" /><!-- 进度条压下状态图片 -->
<attr name="seekbar_edgeDrawable" format="reference|color" /><!-- 进度条默认状态描边图片 -->
<attr name="seekbar_edgeColorPressed" format="color" /><!-- 进度条压下状态描边颜色 -->
<attr name="seekbar_thumbColor" format="color" />
<attr name="seekbar_thumbDrawable" format="reference" />
<attr name="seekbar_trackHeight" format="dimension" /><!-- 轨道默认高度 -->
<attr name="seekbar_edgeWidth" format="dimension" />
<attr name="seekbar_thumbPadding" format="dimension" /><!-- 滑块padding -->
<attr name="seekbar_progressPadding" format="dimension" /><!-- 进度条padding -->
<attr name="seekbar_max" format="integer" />
<attr name="seekbar_progress" format="integer" />
<attr name="seekbar_orientation" format="enum">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
</declare-styleable>
</resources>
```
```
<com.example.myapplicationwithg.CustomSeekBar
android:id="@+id/customSeekBar99"
android:layout_width="300dp"
android:layout_height="64dp"
android:layout_marginVertical="16dp"
android:layout_marginStart="10dp"
android:layout_marginBottom="100dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:seekbar_edgeColorPressed="#4d9df4"
app:seekbar_edgeDrawable="@drawable/gradient_tri_stage"
app:seekbar_edgeWidth="2dp"
app:seekbar_max="100"
app:seekbar_orientation="horizontal"
app:seekbar_progress="50"
app:seekbar_progressColorPressed="#3A77F0"
app:seekbar_progressDrawable="@drawable/gradient_custom"
app:seekbar_progressPadding="4dp"
app:seekbar_thumbColor="#FFFFFF"
app:seekbar_thumbPadding="4dp"
app:seekbar_trackColor="#d0d3da"
app:seekbar_trackHeight="36dp" />
```
`