Android 圆弧形 SeekBar
效果预览
package com.gcssloop.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.SweepGradient;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import com.gcssloop.arcseekbar.R;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;
public class ArcSeekBar extends View {
private static final int DEFAULT_EDGE_LENGTH = 260; // 默认宽高
private static final float CIRCLE_ANGLE = 360; // 圆周角
private static final int DEFAULT_ARC_WIDTH = 40; // 默认宽度 dp
private static final float DEFAULT_OPEN_ANGLE = 120; // 开口角度
private static final float DEFAULT_ROTATE_ANGLE = 90; // 旋转角度
private static final int DEFAULT_BORDER_WIDTH = 0; // 默认描边宽度
private static final int DEFAULT_BORDER_COLOR = 0xffffffff; // 默认描边颜色
private static final int DEFAULT_THUMB_COLOR = 0xffffffff; // 拖动按钮颜色
private static final int DEFAULT_THUMB_WIDTH = 2; // 拖动按钮描边宽度 dp
private static final int DEFAULT_THUMB_RADIUS = 15; // 拖动按钮半径 dp
private static final int DEFAULT_THUMB_SHADOW_RADIUS = 0; // 拖动按钮阴影半径 dp
private static final int DEFAULT_THUMB_SHADOW_COLOR = 0xFF000000; // 拖动按钮阴影颜色
private static final int DEFAULT_SHADOW_RADIUS = 0; // 默认阴影半径 dp
private static final int THUMB_MODE_STROKE = 0; // 拖动按钮模式 - 描边
private static final int THUMB_MODE_FILL = 1; // 拖动按钮模式 - 填充
private static final int THUMB_MODE_FILL_STROKE = 2; // 拖动按钮模式 - 填充+描边
private static final int DEFAULT_MAX_VALUE = 100; // 默认最大数值
private static final int DEFAULT_MIN_VALUE = 0; // 默认最小数值
private static final String KEY_PROGRESS_PRESENT = "PRESENT"; // 用于存储和获取当前百分比
// 可配置数据
private int[] mArcColors; // Seek 颜色
private float mArcWidth; // Seek 宽度
private float mOpenAngle; // 开口的角度大小 0 - 360
private float mRotateAngle; // 旋转角度
private int mBorderWidth; // 描边宽度
private int mBorderColor; // 描边颜色
private int mThumbColor; // 拖动按钮颜色
private float mThumbWidth; // 拖动按钮宽度
private float mThumbRadius; // 拖动按钮半径
private float mThumbShadowRadius;// 拖动按钮阴影半径
private int mThumbShadowColor;// 拖动按钮阴影颜色
private int mThumbMode; // 拖动按钮模式
private int mShadowRadius; // 阴影半径
private int mMaxValue; // 最大数值
private int mMinValue; // 最小数值
private float mCenterX; // 圆弧 SeekBar 中心点 X
private float mCenterY; // 圆弧 SeekBar 中心点 Y
private float mThumbX; // 拖动按钮 中心点 X
private float mThumbY; // 拖动按钮 中心点 Y
private Path mSeekPath;
private Path mBorderPath;
private Paint mArcPaint;
private Paint mThumbPaint;
private Paint mBorderPaint;
private Paint mShadowPaint;
private float[] mTempPos;
private float[] mTempTan;
private PathMeasure mSeekPathMeasure;
private float mProgressPresent = 0; // 当前进度百分比
private boolean mCanDrag = false; // 是否允许拖动
private boolean mAllowTouchSkip = false; // 是否允许越过边界
private GestureDetector mDetector;
private Matrix mInvertMatrix; // 逆向 Matrix, 用于计算触摸坐标和绘制坐标的转换
private Region mArcRegion; // ArcPath的实际区域大小,用于判定单击事件
public ArcSeekBar(Context context) {
this(context, null);
}
public ArcSeekBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ArcSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setSaveEnabled(true);
setLayerType(LAYER_TYPE_SOFTWARE, null);
initAttrs(context, attrs);
initData();
initPaint();
}
// 初始化各种属性
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ArcSeekBar);
mArcColors = getArcColors(context, ta);
mArcWidth = ta.getDimensionPixelSize(R.styleable.ArcSeekBar_arc_width, dp2px(DEFAULT_ARC_WIDTH));
mOpenAngle = ta.getFloat(R.styleable.ArcSeekBar_arc_open_angle, DEFAULT_OPEN_ANGLE);
mRotateAngle = ta.getFloat(R.styleable.ArcSeekBar_arc_rotate_angle, DEFAULT_ROTATE_ANGLE);
mMaxValue = ta.getInt(R.styleable.ArcSeekBar_arc_max, DEFAULT_MAX_VALUE);
mMinValue = ta.getInt(R.styleable.ArcSeekBar_arc_min, DEFAULT_MIN_VALUE);
// 如果用户设置的最大值和最小值不合理,则直接按照默认进行处理
if (mMaxValue <= mMinValue) {
mMaxValue = DEFAULT_MAX_VALUE;
mMinValue = DEFAULT_MIN_VALUE;
}
int progress = ta.getInt(R.styleable.ArcSeekBar_arc_progress, mMinValue);
setProgress(progress);
mBorderWidth = ta.getDimensionPixelSize(R.styleable.ArcSeekBar_arc_border_width, dp2px(DEFAULT_BORDER_WIDTH));
mBorderColor = ta.getColor(R.styleable.ArcSeekBar_arc_border_color, DEFAULT_BORDER_COLOR);
mThumbColor = ta.getColor(R.styleable.ArcSeekBar_arc_thumb_color, DEFAULT_THUMB_COLOR);
mThumbRadius = ta.getDimensionPixelSize(R.styleable.ArcSeekBar_arc_thumb_radius, dp2px(DEFAULT_THUMB_RADIUS));
mThumbShadowRadius = ta.getDimensionPixelSize(R.styleable.ArcSeekBar_arc_thumb_shadow_radius, dp2px(DEFAULT_THUMB_SHADOW_RADIUS));
mThumbShadowColor = ta.getColor(R.styleable.ArcSeekBar_arc_thumb_shadow_color, DEFAULT_THUMB_SHADOW_COLOR);
mThumbWidth = ta.getDimensionPixelSize(R.styleable.ArcSeekBar_arc_thumb_width, dp2px(DEFAULT_THUMB_WIDTH));
mThumbMode = ta.getInt(R.styleable.ArcSeekBar_arc_thumb_mode, THUMB_MODE_STROKE);
mShadowRadius = ta.getDimensionPixelSize(R.styleable.ArcSeekBar_arc_shadow_radius, dp2px(DEFAULT_SHADOW_RADIUS));
ta.recycle();
}
// 获取 Arc 颜色数组
private int[] getArcColors(Context context, TypedArray ta) {
int[] ret;
int resId = ta.getResourceId(R.styleable.ArcSeekBar_arc_colors, 0);
if (0 == resId) {
resId = R.array.arc_colors_default;
}
ret = getColorsByArrayResId(context, resId);
return ret;
}
// 根据 resId 获取颜色数组
private int[] getColorsByArrayResId(Context context, int resId) {
int[] ret;
TypedArray colorArray = context.getResources().obtainTypedArray(resId);
ret = new int[colorArray.length()];
for (int i = 0; i < colorArray.length(); i++) {
ret[i] = colorArray.getColor(i, 0);
}
return ret;
}
// 初始化数据
private void initData() {
mSeekPath = new Path();
mBorderPath = new Path();
mSeekPathMeasure = new PathMeasure();
mTempPos = new float[2];
mTempTan = new float[2];
mDetector = new GestureDetector(getContext(), new OnClickListener());
mInvertMatrix = new Matrix();
mArcRegion = new Region();
}
// 初始化画笔
private void initPaint() {
initArcPaint();
initThumbPaint();
initBorderPaint();
initShadowPaint();
}
// 初始化圆弧画笔
private void initArcPaint() {
mArcPaint = new Paint();
mArcPaint.setAntiAlias(true);
mArcPaint.setStrokeWidth(mArcWidth);
mArcPaint.setStyle(Paint.Style.STROKE);
mArcPaint.setStrokeCap(Paint.Cap.ROUND);
}
// 初始化拖动按钮画笔
private void initThumbPaint() {
mThumbPaint = new Paint();
mThumbPaint.setAntiAlias(true);
mThumbPaint.setColor(mThumbColor);
mThumbPaint.setStrokeWidth(mThumbWidth);
mThumbPaint.setStrokeCap(Paint.Cap.ROUND);
if (mThumbMode == THUMB_MODE_FILL) {
mThumbPaint.setStyle(Paint.Style.FILL_AND_STROKE);
} else if (mThumbMode == THUMB_MODE_FILL_STROKE) {
mThumbPaint.setStyle(Paint.Style.FILL_AND_STROKE);
} else {
mThumbPaint.setStyle(Paint.Style.STROKE);
}
mThumbPaint.setTextSize(56);
}
// 初始化拖动按钮画笔
private void initBorderPaint() {
mBorderPaint = new Paint();
mBorderPaint.setAntiAlias(true);
mBorderPaint.setColor(mBorderColor);
mBorderPaint.setStrokeWidth(mBorderWidth);
mBorderPaint.setStyle(Paint.Style.STROKE);
}
// 初始化阴影画笔
private void initShadowPaint() {
mShadowPaint = new Paint();
mShadowPaint.setAntiAlias(true);
mShadowPaint.setStrokeWidth(mBorderWidth);
mShadowPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("superState", super.onSaveInstanceState());
bundle.putFloat(KEY_PROGRESS_PRESENT, mProgressPresent);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
this.mProgressPresent = bundle.getFloat(KEY_PROGRESS_PRESENT);
state = bundle.getParcelable("superState");
}
if (null != mOnProgressChangeListener) {
mOnProgressChangeListener.onProgressChanged(this, getProgress(), false);
}
super.onRestoreInstanceState(state);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int ws = MeasureSpec.getSize(widthMeasureSpec); //取出宽度的确切数值
int wm = MeasureSpec.getMode(widthMeasureSpec); //取出宽度的测量模式
int hs = MeasureSpec.getSize(heightMeasureSpec); //取出高度的确切数值
int hm = MeasureSpec.getMode(heightMeasureSpec); //取出高度的测量模
if (wm == MeasureSpec.UNSPECIFIED) {
wm = MeasureSpec.EXACTLY;
ws = dp2px(DEFAULT_EDGE_LENGTH);
} else if (wm == MeasureSpec.AT_MOST) {
wm = MeasureSpec.EXACTLY;
ws = Math.min(dp2px(DEFAULT_EDGE_LENGTH), ws);
}
if (hm == MeasureSpec.UNSPECIFIED) {
hm = MeasureSpec.EXACTLY;
hs = dp2px(DEFAULT_EDGE_LENGTH);
} else if (hm == MeasureSpec.AT_MOST) {
hm = MeasureSpec.EXACTLY;
hs = Math.min(dp2px(DEFAULT_EDGE_LENGTH), hs);
}
setMeasuredDimension(MeasureSpec.makeMeasureSpec(ws, wm), MeasureSpec.makeMeasureSpec(hs, hm));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 计算在当前大小下,内容应该显示的大小和起始位置
int safeW = w - getPaddingLeft() - getPaddingRight();
int safeH = h - getPaddingTop() - getPaddingBottom();
float edgeLength, startX, startY;
float fix = mArcWidth / 2 + mBorderWidth + mShadowRadius * 2; // 修正距离,画笔宽度的修正
if (safeW < safeH) {
// 宽度小于高度,以宽度为准
edgeLength = safeW - fix;
startX = getPaddingLeft();
startY = (safeH - safeW) / 2.0f + getPaddingTop();
} else {
// 宽度大于高度,以高度为准
edgeLength = safeH - fix;
startX = (safeW - safeH) / 2.0f + getPaddingLeft();
startY = getPaddingTop();
}
// 得到显示区域和中心的
RectF content = new RectF(startX + fix, startY + fix, startX + edgeLength, startY + edgeLength);
mCenterX = content.centerX();
mCenterY = content.centerY();
// 得到路径
mSeekPath.reset();
mSeekPath.addArc(content, mOpenAngle / 2, CIRCLE_ANGLE - mOpenAngle);
mSeekPathMeasure.setPath(mSeekPath, false);
computeThumbPos(mProgressPresent);
resetShaderColor();
mInvertMatrix.reset();
mInvertMatrix.preRotate(-mRotateAngle, mCenterX, mCenterY);
mArcPaint.getFillPath(mSeekPath, mBorderPath);
mBorderPath.close();
mArcRegion.setPath(mBorderPath, new Region(0, 0, w, h));
}
// 重置 shader 颜色
private void resetShaderColor() {
// 计算渐变数组
float startPos = (mOpenAngle / 2) / CIRCLE_ANGLE;
float stopPos = (CIRCLE_ANGLE - (mOpenAngle / 2)) / CIRCLE_ANGLE;
int len = mArcColors.length - 1;
float distance = (stopPos - startPos) / len;
float pos[] = new float[mArcColors.length];
for (int i = 0; i < mArcColors.length; i++) {
pos[i] = startPos + (distance * i);
}
SweepGradient gradient = new SweepGradient(mCenterX, mCenterY, mArcColors, pos);
mArcPaint.setShader(gradient);
}
// 具体绘制
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.rotate(mRotateAngle, mCenterX, mCenterY);
mShadowPaint.setShadowLayer(mShadowRadius * 2, 0, 0, getColor());
canvas.drawPath(mBorderPath, mShadowPaint);
canvas.drawPath(mSeekPath, mArcPaint);
if (mBorderWidth > 0) {
canvas.drawPath(mBorderPath, mBorderPaint);
}
if (mThumbShadowRadius > 0) {
mThumbPaint.setShadowLayer(mThumbShadowRadius, 0, 0, mThumbShadowColor);
canvas.drawCircle(mThumbX, mThumbY, mThumbRadius, mThumbPaint);
mThumbPaint.clearShadowLayer();
}
canvas.drawCircle(mThumbX, mThumbY, mThumbRadius, mThumbPaint);
canvas.restore();
}
private boolean moved = false;
private int lastProgress = -1;
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
int action = event.getActionMasked();
switch (action) {
case ACTION_DOWN:
moved = false;
judgeCanDrag(event);
if (null != mOnProgressChangeListener) {
mOnProgressChangeListener.onStartTrackingTouch(this);
}
break;
case ACTION_MOVE:
if (!mCanDrag) {
break;
}
float tempProgressPresent = getCurrentProgress(event.getX(), event.getY());
if (!mAllowTouchSkip) {
// 不允许突变
if (Math.abs(tempProgressPresent - mProgressPresent) > 0.5f) {
break;
}
}
// 允许突变 或者非突变
mProgressPresent = tempProgressPresent;
computeThumbPos(mProgressPresent);
// 事件回调
if (null != mOnProgressChangeListener && getProgress() != lastProgress) {
mOnProgressChangeListener.onProgressChanged(this, getProgress(), true);
lastProgress = getProgress();
}
moved = true;
break;
case ACTION_UP:
case ACTION_CANCEL:
if (null != mOnProgressChangeListener && moved) {
mOnProgressChangeListener.onStopTrackingTouch(this);
}
break;
}
mDetector.onTouchEvent(event);
invalidate();
return true;
}
// 判断是否允许拖动
private void judgeCanDrag(MotionEvent event) {
float[] pos = {event.getX(), event.getY()};
mInvertMatrix.mapPoints(pos);
if (getDistance(pos[0], pos[1]) <= mThumbRadius * 1.5) {
mCanDrag = true;
} else {
mCanDrag = false;
}
}
private class OnClickListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent e) {
// 判断是否点击在了进度区域
if (!isInArcProgress(e.getX(), e.getY())) return false;
// 点击允许突变
mProgressPresent = getCurrentProgress(e.getX(), e.getY());
computeThumbPos(mProgressPresent);
// 事件回调
if (null != mOnProgressChangeListener) {
mOnProgressChangeListener.onProgressChanged(ArcSeekBar.this, getProgress(), true);
mOnProgressChangeListener.onStopTrackingTouch(ArcSeekBar.this);
}
return true;
}
}
// 判断该点是否在进度条上面
private boolean isInArcProgress(float px, float py) {
float[] pos = {px, py};
mInvertMatrix.mapPoints(pos);
return mArcRegion.contains((int) pos[0], (int) pos[1]);
}
// 获取当前进度理论进度数值
private float getCurrentProgress(float px, float py) {
float diffAngle = getDiffAngle(px, py);
float progress = diffAngle / (CIRCLE_ANGLE - mOpenAngle);
if (progress < 0) progress = 0;
if (progress > 1) progress = 1;
return progress;
}
// 获得当前点击位置所成角度与开始角度之间的数值差
private float getDiffAngle(float px, float py) {
float angle = getAngle(px, py);
float diffAngle;
diffAngle = angle - mRotateAngle;
if (diffAngle < 0) {
diffAngle = (diffAngle + CIRCLE_ANGLE) % CIRCLE_ANGLE;
}
diffAngle = diffAngle - mOpenAngle / 2;
return diffAngle;
}
// 计算指定位置与内容区域中心点的夹角
private float getAngle(float px, float py) {
float angle = (float) ((Math.atan2(py - mCenterY, px - mCenterX)) * 180 / 3.14f);
if (angle < 0) {
angle += 360;
}
return angle;
}
// 计算指定位置与上次位置的距离
private float getDistance(float px, float py) {
return (float) Math.sqrt((px - mThumbX) * (px - mThumbX) + (py - mThumbY) * (py - mThumbY));
}
private int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getContext().getResources().getDisplayMetrics());
}
// 计算拖动块应该显示的位置
private void computeThumbPos(float present) {
if (present < 0) present = 0;
if (present > 1) present = 1;
if (null == mSeekPathMeasure) return;
float distance = mSeekPathMeasure.getLength() * present;
mSeekPathMeasure.getPosTan(distance, mTempPos, mTempTan);
mThumbX = mTempPos[0];
mThumbY = mTempPos[1];
}
/**
* 获取当前进度的具体颜色
*
* @return 当前进度在渐变中的颜色
*/
public int getColor() {
return getColor(mProgressPresent);
}
/**
* 获取某个百分比位置的颜色
*
* @param radio 取值[0,1]
* @return 最终颜色
*/
private int getColor(float radio) {
float diatance = 1.0f / (mArcColors.length - 1);
int startColor;
int endColor;
if (radio >= 1) {
return mArcColors[mArcColors.length - 1];
}
for (int i = 0; i < mArcColors.length; i++) {
if (radio <= i * diatance) {
if (i == 0) {
return mArcColors[0];
}
startColor = mArcColors[i - 1];
endColor = mArcColors[i];
float areaRadio = getAreaRadio(radio, diatance * (i - 1), diatance * i);
return getColorFrom(startColor, endColor, areaRadio);
}
}
return -1;
}
/**
* 计算当前比例在子区间的比例
*
* @param radio 总比例
* @param startPosition 子区间开始位置
* @param endPosition 子区间结束位置
* @return 自区间比例[0, 1]
*/
private float getAreaRadio(float radio, float startPosition, float endPosition) {
return (radio - startPosition) / (endPosition - startPosition);
}
/**
* 取两个颜色间的渐变区间 中的某一点的颜色
*
* @param startColor 开始的颜色
* @param endColor 结束的颜色
* @param radio 比例 [0, 1]
* @return 选中点的颜色
*/
private int getColorFrom(int startColor, int endColor, float radio) {
int redStart = Color.red(startColor);
int blueStart = Color.blue(startColor);
int greenStart = Color.green(startColor);
int redEnd = Color.red(endColor);
int blueEnd = Color.blue(endColor);
int greenEnd = Color.green(endColor);
int red = (int) (redStart + ((redEnd - redStart) * radio + 0.5));
int greed = (int) (greenStart + ((greenEnd - greenStart) * radio + 0.5));
int blue = (int) (blueStart + ((blueEnd - blueStart) * radio + 0.5));
return Color.argb(255, red, greed, blue);
}
/**
* 设置进度
*
* @param progress 进度值
*/
public void setProgress(int progress) {
System.out.println("setProgress = " + progress);
if (progress > mMaxValue) progress = mMaxValue;
if (progress < mMinValue) progress = mMinValue;
mProgressPresent = (progress - mMinValue) * 1.0f / (mMaxValue - mMinValue);
System.out.println("setProgress present = " + mProgressPresent);
if (null != mOnProgressChangeListener) {
mOnProgressChangeListener.onProgressChanged(this, progress, false);
}
computeThumbPos(mProgressPresent);
postInvalidate();
}
/**
* 获取当前进度数值
*
* @return 当前进度数值
*/
public int getProgress() {
return (int) (mProgressPresent * (mMaxValue - mMinValue)) + mMinValue;
}
/**
* 设置颜色
*
* @param colors 颜色
*/
public void setArcColors(int[] colors) {
mArcColors = colors;
resetShaderColor();
postInvalidate();
}
/**
* 设置最大数值
* @param max 最大数值
*/
public void setMaxValue(int max) {
mMaxValue = max;
}
/**
* 设置最小数值
* @param min 最小数值
*/
public void setMinValue(int min) {
mMinValue = min;
}
/**
* 设置颜色
*
* @param colorArrayRes 颜色资源 R.array.arc_color
*/
public void setArcColors(int colorArrayRes) {
setArcColors(getColorsByArrayResId(getContext(), colorArrayRes));
}
private OnProgressChangeListener mOnProgressChangeListener;
public void setOnProgressChangeListener(OnProgressChangeListener onProgressChangeListener) {
mOnProgressChangeListener = onProgressChangeListener;
}
public interface OnProgressChangeListener {
/**
* 进度发生变化
*
* @param seekBar 拖动条
* @param progress 当前进度数值
* @param isUser 是否是用户操作, true 表示用户拖动, false 表示通过代码设置
*/
void onProgressChanged(ArcSeekBar seekBar, int progress, boolean isUser);
/**
* 用户开始拖动
*
* @param seekBar 拖动条
*/
void onStartTrackingTouch(ArcSeekBar seekBar);
/**
* 用户结束拖动
*
* @param seekBar 拖动条
*/
void onStopTrackingTouch(ArcSeekBar seekBar);
}
}
attrs.xml代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ArcSeekBar">
<attr name="arc_width" format="dimension|reference" />
<attr name="arc_open_angle" format="float" />
<attr name="arc_rotate_angle" format="float" />
<attr name="arc_colors" format="reference" />
<attr name="arc_border_width" format="dimension|reference" />
<attr name="arc_border_color" format="color|reference" />
<attr name="arc_max" format="integer|reference" />
<attr name="arc_min" format="integer|reference" />
<attr name="arc_progress" format="integer|reference" />
<attr name="arc_thumb_width" format="dimension|reference" />
<attr name="arc_thumb_color" format="color|reference" />
<attr name="arc_thumb_radius" format="dimension|reference" />
<attr name="arc_thumb_shadow_radius" format="dimension|reference" />
<attr name="arc_thumb_shadow_color" format="color|reference" />
<attr name="arc_thumb_mode" format="integer|dimension">
<enum name="STROKE" value="0" />
<enum name="FILL" value="1" />
<enum name="FILL_STROKE" value="2" />
</attr>
<attr name="arc_shadow_radius" format="dimension|reference" />
</declare-styleable>
</resources>
相关文章:
Android 圆弧形 SeekBar
效果预览package com.gcssloop.widget;import android.annotation.SuppressLint;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Matrix;import android.graph…...
java 字典
java 字典 数据结构总览 Map Map 描述的是一种映射关系,一个 key 对应一个 value,可以添加,删除,修改和获取 key/value,util 提供了多种 Map HashMap: hash 表实现的 map,插入删除查找性能都是 O(1)&…...
【企业服务器LNMP环境搭建】mysql安装
MySQL安装步骤: 1、相关说明 1.1、编译参数的说明 -DCMAKE_INSTALL_PREFIX安装到的软件目录-DMYSQL_DATADIR数据文件存储的路径-DSYSCONFDIR配置文件路径 (my.cnf)-DENABLED_LOCAL_INFILE1使用localmysql客户端的配置-DWITH_PARTITION_STORAGE_ENGINE使mysql支持…...
vue自定义指令以及angular自定义指令(以禁止输入空格为例)
哈喽,小伙伴们,大家好啊,最近要实现一个vue自定义指令,就是让input输入框禁止输入空格建立一个directives的指令文件,里面专门用来建立各个指令的官方文档:自定义指令 | Vue.js (vuejs.org)我们都知道vue中…...
异常 复习
异常复习 异常(广义):泛指程序中一切不正常的情况 错误:例如内存不够用,程序是无法解决的 异常(狭义):程序在运行中出现问题,但是可以通过异常处理机制处理,程序可以继续向后执行 异常体系 Throwable类有两个直接子类:Excepti…...
K8s:开源安全平台 kubescape 实现 Pod 的安全合规检查/镜像漏洞扫描
写在前面 生产环境中的 k8s 集群安全不可忽略,即使是内网环境容器化的应用部署虽然本质上没有变化,始终是机器上的一个进程但是提高了安全问题的处理的复杂性分享一个开源的 k8s 集群安全合规检查/漏洞扫描 工具 kubescape博文内容涉及: kube…...
C#中,FTP同步或异步读取大量文件
一次快速读取上万个文件中的内容 在C#中,可以使用FTP客户端类(如FtpWebRequest)来连接FTP服务器并进行文件操作。一次快速读取上万个文件中的内容,可以采用多线程的方式并发读取文件。 以下是一个示例代码,用于读取FT…...
STM32单片机的FLASH和RAM
STM32内置有Flash和RAM(而RAM分为SRAM和DRAM,STM32内为SRAM),硬件上他们是不同的设备存储器、属于两个器件,但这两个存储器的寄存器输入输出端口被组织在同一个虚拟线性地址空间内。 MDK预处理、编译、汇编、链接后编…...
Java 二叉树的遍历
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问依次且仅被访问一次。前序遍历(根 左 右)先访问根结点,然后前序遍历左子树…...
实习日记-C#
数据类型 字符串常量 string a "hello, world"; // hello, world string b "hello, world"; // hello, world string c "hello \t world"; // hello world string d "hello \t wor…...
Tech Lead如何引导团队成员解决问题?
作为一个开发团队的Tech Lead,当团队成员向你寻求帮助时,你有没有说过下面这些话? 你别管了,我来解决这个问题你只要。。。就行了你先做其他的吧,我研究一下,然后告诉你怎么做 当我们说这些话时ÿ…...
07--组件
一、小程序组件分类微信团队为开发者提供了一系列基础组件,开发者可以通过组合这些基础组件进行快速开发。小程序中的组件也是非常丰富的,开发者可以基于组件快速搭建出漂亮的页面结构。小程序中的组件其实相当于网页中的HTML标签,只不过标签…...
怎么做好一个完整的项目复盘
复盘,是运营必不可少的能力,小到一次买菜的经历,大到百亿千亿的投资项目,都可以通过复盘来总结规律、提升水平。简单说来,复盘可以达到的效果有两条:优化弱项,强化强项明确自己的价值࿰…...
浅谈一下mysql8.0与5.7的字符集
修改字符集 修改步骤 在MySQL8.0版本之前,默认字符集为1atin1,utf8字符集指向的是utf8mb3。网站开发人员在数据库设计的时候往往会将编码修改为ut8字符集。如果遗忘修改默认的编码,就会出现乱码的问题。从MySQL8.0开始,数据库的默认编码将改…...
paddle推理部署(cpu)
我没按照官方文档去做,吐槽一下,官方文档有点混乱。。一、概述总结起来,就是用c示例代码,用一个模型做推理。二、示例代码下载https://www.paddlepaddle.org.cn/paddle/paddleinferencehttps://github.com/PaddlePaddle/Paddle-In…...
想开发IM集群?先搞懂什么是RPC!
即时通讯网官方技术群和社区里,经常有开发者在纠结怎么开发IM集群,虽然真正的使用人数,可能用个人电脑单机都能支撑。你也许会说,明明不需要用到IM集群,干吗要自找麻烦?答曰:“老板说这个得有&a…...
案例13-前端对localStorage的使用分析
一:背景介绍 前端在调用后端接口获取某一个人的评论次数、获赞次数、回复次数。调用之后判断后端返回过来的值。如果返回回来的值是0的话,从缓存中获取对应的值,如果从缓存中获取的评论次数为空那么其他两个的次数也为0。 二:思路…...
CNNIC第51次中国互联网络发展状况统计报告用户规模变化发布、解读与白杨SEO看法
一、第51次《中国互联网络发展状况统计报告》发布 3月2日,中国互联网络信息中心(简称CNNIC)在京发布第51次《中国互联网络发展状况统计报告》。《报告》显示,截至2022年12月,我国网民规模达10.67亿,较2021…...
【数据结构】单链表的实现
本篇主要总结单链表是如何实现的,数据结构是如何管理数据的,详细的介绍每一步是如何实现以及各种注意事项。🚀1.单链表的实现🚀🍭1.1单链表的尾插🍭1.2单链表的头插🍭1.3单链表的打印dz…...
从0到1做产品!产品设计的6个步骤
相信不少产品经理在刚入行时,都遇到过这样的情况: 接到需求后不知所措,然后下意识地照着竞品开始盲目地画原型。 其实,这样的设计过程不仅缺乏逻辑性,在后续阶段也很容易出现各种问题。 在此,跟大家分享一下…...
ESP32遥控器软硬件设计
一. 前言 做智能车 或者 四轴飞控怎么能少得了遥控器呢!在这里给大家分享一个简单的基于ESP32遥控器的设计,包括软硬件以及3D外壳。 二. 硬件设计 1. 功能介绍 遥控器嘛,通信方式是最重要的,本设计支持 WIFI、蓝牙 和 2.4G&…...
vue-template-admin的keep-alive缓存与移除缓存
一,场景 A页面是表单页面,填写后需要跳转B页面。如果B页面不操作返回的话,应该能还原A页面的内容,而如果B页面点击提交,再回到A页面的时候,应该清除缓存。 二,实现方法 A页面要缓存数据&…...
【人工智能 AI】机器学习快速入门教程(Google)
目录 机器学习术语 标签 特性 示例 模型 回归与分类 深入了解机器学习:线性回归 深入了解机器学习:训练和损失 平方损失函数:一种常用的损失函数 机器学习术语 预计用时:8 分钟 什么是(监督式ÿ…...
适配器模式
概览 适配器模式是一种结构型设计模式,用于将一个类的接口转换为客户端所期望的另一种接口。通常情况下,这种转换是由一个适配器类完成的,适配器类包装了原始类,并实现了客户端所期望的接口。这种模式非常适用于在不修改现有代码…...
00后跨专业学软件测试,斩获8.5K高薪逆袭职场
我想说的第一句:既然有梦想,就应该去拼搏还记得,我大学毕业前,就已经暗下决心到xxx培训机构接受培训。那个时候,没有任何海同公司的人主动找我或者联系过我,我是自己在网上发现了xxxx培训机构的!…...
数据结构和算法学习
文章目录精通一个领域切题四件套算法算法的五个条件流程图数据结构数据与信息数据信息数据结构和算法数据结构算法时间复杂度空间复杂度数组 Array优点缺点数组和链表的区别时间复杂度链表 Linked List优点缺点时间复杂度单向链表双向链表循环链表双向循环链表堆栈 Stack队列 Q…...
剑指 Offer II 012. 左右两边子数组的和相等
题目链接 剑指 Offer II 012. 左右两边子数组的和相等 easy 题目描述 给你一个整数数组 nums,请计算数组的 中心下标 。 数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。 如果中心下标位于数组最左端,那…...
Java货物摆放
题目描述 小蓝有一个超大的仓库,可以摆放很多货物。 现在,小蓝有 � n 箱货物要摆放在仓库,每箱货物都是规则的正方体。小蓝规定了长、宽、高三个互相垂直的方向,每箱货物的边都必须严格平行于长、宽、高。 小蓝希望所…...
计算机求解满足三角形各边数字之和相等的数字填充
圆圈处不重复的填入1至9,使得每条边的四个数字相加的总和相等。 求解思路: 数组中存放1到9的数字,每次随机交换两个数字,构建出新的数字组合,计算这个数字组合是否符合要求。 #include <stdio.h> #include <…...
python魔术方法
魔术方法 魔术方法就是一个类中的方法,和普通方法唯一的不同是普通方法需要调用,而魔术方法是在特定时刻自动触发。这些魔术方法的名字特定,不能更改,但是入口参数的名字可以自己命名。 基本魔术方法 new(cls[,…]) _new_ 是在…...
网络营销网站的建设与策划/济南优化网络营销
Django处理一个请求 项目启动后根据 settings ROOT_URLCONF 决定项目根URLconf urlpatterns是django.conf.urls.url()实例的一个Python列表 Django依次匹配每个URL模式,匹配成功后就停止 Django匹配成功,调用相应视图函数(或一个基于类的视图)&#…...
html网站素材网/企业网站搜索优化网络推广
哨兵模式1 简介作用:①Master状态检测②如果Master异常,则会进行Master-Slave切换,将其中一个Slave作为Master,将之前的Master作为Slave下线:①主观下线:Subjectively Down,简称 SDOWNÿ…...
ipfs做网站/重大新闻事件2023
当您在使用LC-MS/MS进样测试的过程中出现目标物未出峰的问题时,如果系统配置内有TUV等紫外检测器,可通过对比紫外色谱图数据是否正常来快速排查问题是发生在LC侧还是MS侧。可参考以下步骤快速排查。LC端1、检查样品的前处理是否正确?如溶解样…...
软件工程师就业前景/班级优化大师下载安装app
一小伙工作快3年了,拿到了阿里云Java开发岗位P6的offer,算HR面一起,加起来有7轮面试了,将近3个月的时间,什么jvm、多线程编程、Linux、网络等方面的面试题,直接面试到自己怀疑人生。而自己跟HR谈论薪资的时…...
wordpress升级流程/手机优化游戏性能的软件
文章目录分层思想OSI七层模型TCP/IP五层协议簇数据的封装与解封装过程设备与层之间的对应关系各层之间通信协议与层之间的对应关系TCP/IP四层模型分层思想 通信需求---->定义协议(规则)标准 完成一件事需要的协议太多!就需要进行分层&…...
网页空间层次/seo服务价格表
这一系列文章原载于公众号 工程师milter,如果文章对大家有帮助,恳请大家动手关注下哈~matplotlib是优秀的python画图工具,功能十分强大,但是使用却很复杂。你有没有如下的经历:1、图形只差一点点就满足你的要求,可是怎…...