Android——自定义带刻度的SeekBar单向拖动条
时间过得真快,才发现好久没来逛逛了。没写博客的这段时间一直在做项目,连续完成了两个大型app,这个过程很享受,这是独立开发的,所以中途有很多很多的问题需要自己一个一个的去解决,现在接近尾声了,发现自己在这个阶段成长了不少,当然需要学习的知识还有很多很多,就让我们大家一起学习吧!
今天就分享一个自己在项目中,客户要求的功能,拖动条设置ListView列表中item的金额。这边主要的就是说seekbar这个东西,那我们开始吧!
看下效果:
大概就是这样,上面的刻度值是可以动态设置的,下面详细说一下,先看下自定义的代码块:
package com.ds.platform.view;import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.NinePatch;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.Typeface;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;import com.ds.platform.R;
import com.ds.platform.utils.SharePreferencesUtils;/*** @author: Allen* @date: 2017/3/13* @description: 自定义SeekBar 带刻度*/
public class RangeSeekBar extends View {private static final float DEFAULT_RADIUS = 0.5f;//default seekbar's padding left and rightprivate int DEFAULT_PADDING_LEFT_AND_RIGHT;private int defaultPaddingTop;//进度提示的背景 The background of the progressprivate final int mProgressHintBGId;// 按钮的背景 The background of the Drag buttonprivate final int mThumbResId;//刻度模式:number根据数字实际比例排列;other 均分排列//Scale mode:// number according to the actual proportion of the number of arranged;// other equally arrangedprivate final int mCellMode;//single是Seekbar模式,range是RangeSeekbar//single is Seekbar mode, range is angeSeekbar//single = 1; range = 2private final int mSeekBarMode;//默认为1,当大于1时自动切回刻度模式//The default is 1, and when it is greater than 1,// it will automatically switch back to the scale modeprivate int cellsCount = 1;//刻度与进度条间的间距//The spacing between the scale and the progress barprivate int textPadding;//进度提示背景与按钮之间的距离//The progress indicates the distance between the background and the buttonprivate int mHintBGPadding;private int mSeekBarHeight;private int mThumbSize;//两个按钮之间的最小距离//The minimum distance between two buttonsprivate int reserveCount;private int mCursorTextHeight;private int mPartLength;private int heightNeeded;private int lineCorners;private int lineWidth;//选择过的进度条颜色// the color of the selected progress barprivate int colorLineSelected;//未选则的进度条颜色// the color of the unselected progress barprivate int colorLineEdge;//The foreground color of progress bar and thumb button.private int colorPrimary;//The background color of progress bar and thumb button.private int colorSecondary;//刻度文字与提示文字的大小//Scale text and prompt text sizeprivate int mTextSize;private int mTextColor;private int lineTop, lineBottom, lineLeft, lineRight;//进度提示背景的高度,宽度如果是0的话会自适应调整//Progress prompted the background height, width,// if it is 0, then adaptively adjustprivate float mHintBGHeight;private float mHintBGWith;private float offsetValue;private float cellsPercent;private float reserveValue;private float reservePercent;private float maxValue, minValue;//真实的最大值和最小值//True maximum and minimum valuesprivate float mMin, mMax;private boolean isEnable = true;private final boolean mHideProgressHint;//刻度上显示的文字private CharSequence[] mTextArray;private Bitmap mProgressHintBG;private Paint mMainPaint = new Paint();private Paint mCursorPaint = new Paint();private Paint mProgressPaint;private RectF line = new RectF();private SeekBar leftSB;private SeekBar rightSB;private SeekBar currTouch;private OnRangeChangedListener callback;public RangeSeekBar(Context context) {this(context, null);}public RangeSeekBar(Context context, AttributeSet attrs) {super(context, attrs);TypedArray t = context.obtainStyledAttributes(attrs, R.styleable.RangeSeekBar);cellsCount = t.getInt(R.styleable.RangeSeekBar_cells, 1);reserveValue = t.getFloat(R.styleable.RangeSeekBar_reserve, 0);mMin = t.getFloat(R.styleable.RangeSeekBar_min, 0);//最小取值//从缓存中拿值float maxValue = SharePreferencesUtils.getFloat(context, "maxValue", 0);if (0 == maxValue) {mMax = t.getFloat(R.styleable.RangeSeekBar_max, 100);//最大取值} else {mMax = maxValue;}mThumbResId = t.getResourceId(R.styleable.RangeSeekBar_seekBarResId, 0);mProgressHintBGId = t.getResourceId(R.styleable.RangeSeekBar_progressHintResId, 0);colorLineSelected = t.getColor(R.styleable.RangeSeekBar_lineColorSelected, 0xFF4BD962);colorLineEdge = t.getColor(R.styleable.RangeSeekBar_lineColorEdge, 0xFF0000);colorPrimary = t.getColor(R.styleable.RangeSeekBar_thumbPrimaryColor, 0);colorSecondary = t.getColor(R.styleable.RangeSeekBar_thumbSecondaryColor, 0);//从缓存中拿值CharSequence tempArray[] = SharePreferencesUtils.getStringSet(context, "textArray", null);if (tempArray == null || tempArray.length == 0) {mTextArray = t.getTextArray(R.styleable.RangeSeekBar_markTextArray);} else {mTextArray = tempArray;}mHideProgressHint = t.getBoolean(R.styleable.RangeSeekBar_hideProgressHint, false);textPadding = (int) t.getDimension(R.styleable.RangeSeekBar_textPadding, dp2px(context, 20));mTextSize = (int) t.getDimension(R.styleable.RangeSeekBar_textSize, dp2px(context, 12));mTextColor = t.getColor(R.styleable.RangeSeekBar_textColor, ContextCompat.getColor(context, R.color.main_text));mHintBGHeight = t.getDimension(R.styleable.RangeSeekBar_hintBGHeight, 0);mHintBGWith = t.getDimension(R.styleable.RangeSeekBar_hintBGWith, 0);mSeekBarHeight = (int) t.getDimension(R.styleable.RangeSeekBar_seekBarHeight, dp2px(context, 2));mHintBGPadding = (int) t.getDimension(R.styleable.RangeSeekBar_hintBGPadding, 0);mThumbSize = (int) t.getDimension(R.styleable.RangeSeekBar_thumbSize, dp2px(context, 26));mCellMode = t.getInt(R.styleable.RangeSeekBar_cellMode, 0);mSeekBarMode = t.getInt(R.styleable.RangeSeekBar_seekBarMode, 2);if (mSeekBarMode == 2) {leftSB = new SeekBar(-1);rightSB = new SeekBar(1);} else {leftSB = new SeekBar(-1);}// if you don't set the mHintBGWith or the mHintBGWith < default value, if will use default valueif (mHintBGWith == 0) {DEFAULT_PADDING_LEFT_AND_RIGHT = dp2px(context, 25);} else {DEFAULT_PADDING_LEFT_AND_RIGHT = Math.max((int) (mHintBGWith / 2 + dp2px(context, 5)), dp2px(context, 25));}setRules(mMin, mMax, reserveValue, cellsCount);initPaint();initBitmap();t.recycle();defaultPaddingTop = mSeekBarHeight / 2;mHintBGHeight = mHintBGHeight == 0 ? (mCursorPaint.measureText("国") * 3) : mHintBGHeight;}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int heightSize = MeasureSpec.getSize(heightMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);heightNeeded = 2 * (lineTop) + mSeekBarHeight;/*** onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值* MeasureSpec.EXACTLY 是精确尺寸* MeasureSpec.AT_MOST 是最大尺寸* MeasureSpec.UNSPECIFIED 是未指定尺寸*/if (heightMode == MeasureSpec.EXACTLY) {heightSize = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);} else if (heightMode == MeasureSpec.AT_MOST) {heightSize = MeasureSpec.makeMeasureSpec(heightSize < heightNeeded ? heightSize : heightNeeded, MeasureSpec.EXACTLY);} else {heightSize = MeasureSpec.makeMeasureSpec(heightNeeded, MeasureSpec.EXACTLY);}super.onMeasure(widthMeasureSpec, heightSize);}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);//计算进度条的位置,并根据它初始化两个按钮的位置// Calculates the position of the progress bar and initializes the positions of// the two buttons based on itlineLeft = DEFAULT_PADDING_LEFT_AND_RIGHT + getPaddingLeft();lineRight = w - lineLeft - getPaddingRight();lineTop = (int) mHintBGHeight + mThumbSize / 2 - mSeekBarHeight / 2 + 30;lineBottom = lineTop + mSeekBarHeight;lineWidth = lineRight - lineLeft;line.set(lineLeft, lineTop, lineRight, lineBottom);lineCorners = (int) ((lineBottom - lineTop) * 0.45f);leftSB.onSizeChanged(lineLeft, lineBottom, mThumbSize, lineWidth, cellsCount > 1, mThumbResId, getContext());if (mSeekBarMode == 2) {rightSB.onSizeChanged(lineLeft, lineBottom, mThumbSize, lineWidth, cellsCount > 1, mThumbResId, getContext());}}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);//绘制刻度,并且根据当前位置是否在刻度范围内设置不同的颜色显示// Draw the scales, and according to the current position is set within// the scale range of different color displayif (mTextArray != null) {mPartLength = lineWidth / (mTextArray.length - 1);for (int i = 0; i < mTextArray.length; i++) {final String text2Draw = mTextArray[i].toString();float x;//平分显示if (mCellMode == 1) {mCursorPaint.setColor(mTextColor);mCursorPaint.setAntiAlias(true);x = lineLeft + i * mPartLength - mCursorPaint.measureText(text2Draw) / 2;} else {float num = Float.parseFloat(text2Draw);float[] result = getCurrentRange();if (compareFloat(num, result[0]) != -1 && compareFloat(num, result[1]) != 1 && mSeekBarMode == 2) {mCursorPaint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));} else {mCursorPaint.setColor(mTextColor);mCursorPaint.setAntiAlias(true);}//按实际比例显示x = lineLeft + lineWidth * (num - mMin) / (mMax - mMin)- mCursorPaint.measureText(text2Draw) / 2;}float y = lineTop - textPadding;canvas.drawText(text2Draw, x, y, mCursorPaint);}}//绘制进度条// draw the progress barmMainPaint.setColor(colorLineEdge);canvas.drawRoundRect(line, lineCorners, lineCorners, mMainPaint);mMainPaint.setColor(colorLineSelected);if (mSeekBarMode == 2) {canvas.drawRect(leftSB.left + leftSB.widthSize / 2 + leftSB.lineWidth * leftSB.currPercent, lineTop,rightSB.left + rightSB.widthSize / 2 + rightSB.lineWidth * rightSB.currPercent, lineBottom, mMainPaint);} else {canvas.drawRect(leftSB.left + leftSB.widthSize / 2, lineTop,leftSB.left + leftSB.widthSize / 2 + leftSB.lineWidth * leftSB.currPercent, lineBottom, mMainPaint);}leftSB.draw(canvas);if (mSeekBarMode == 2) {rightSB.draw(canvas);}}/*** 初始化画笔* init the paints*/private void initPaint() {mMainPaint.setStyle(Paint.Style.FILL);mMainPaint.setColor(colorLineEdge);mCursorPaint.setStyle(Paint.Style.FILL);mCursorPaint.setColor(colorLineEdge);mCursorPaint.setTextSize(mTextSize);mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mProgressPaint.setTypeface(Typeface.DEFAULT);mProgressPaint.setColor(0xFF0000);//计算文字的高度//Calculate the height of the textPaint.FontMetrics fm = mCursorPaint.getFontMetrics();mCursorTextHeight = (int) (Math.ceil(fm.descent - fm.ascent) + 2);}/*** 初始化进度提示的背景*/private void initBitmap() {if (mProgressHintBGId != 0) {mProgressHintBG = BitmapFactory.decodeResource(getResources(), mProgressHintBGId);} else {mProgressHintBG = BitmapFactory.decodeResource(getResources(), R.drawable.progress_hint_bg);}}//*********************************** SeekBar ***********************************//private class SeekBar {private int lineWidth;private int widthSize, heightSize;private int left, right, top, bottom;private float currPercent;private float material = 0;public boolean isShowingHint;private boolean isLeft;private Bitmap bmp;private ValueAnimator anim;private RadialGradient shadowGradient;private Paint defaultPaint;private String mHintText2Draw;private Boolean isPrimary = true;public SeekBar(int position) {if (position < 0) {isLeft = true;} else {isLeft = false;}}/*** 计算每个按钮的位置和尺寸* Calculates the position and size of each button** @param x* @param y* @param hSize* @param parentLineWidth* @param cellsMode* @param bmpResId* @param context*/protected void onSizeChanged(int x, int y, int hSize, int parentLineWidth, boolean cellsMode, int bmpResId, Context context) {heightSize = hSize;widthSize = heightSize;left = x - widthSize / 2;right = x + widthSize / 2;top = y - heightSize / 2;bottom = y + heightSize / 10;if (cellsMode) {lineWidth = parentLineWidth;} else {lineWidth = parentLineWidth;}if (bmpResId > 0) {Bitmap original = BitmapFactory.decodeResource(context.getResources(), bmpResId);if (original != null) {Matrix matrix = new Matrix();float scaleHeight = mThumbSize * 1.0f / original.getHeight();float scaleWidth = scaleHeight;matrix.postScale(scaleWidth, scaleHeight);bmp = Bitmap.createBitmap(original, 0, 0, original.getWidth(), original.getHeight(), matrix, true);}} else {defaultPaint = new Paint(Paint.ANTI_ALIAS_FLAG);int radius = (int) (widthSize * DEFAULT_RADIUS);int barShadowRadius = (int) (radius * 0.95f);int mShadowCenterX = widthSize / 2;int mShadowCenterY = heightSize / 2;shadowGradient = new RadialGradient(mShadowCenterX, mShadowCenterY, barShadowRadius, Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP);}}/*** 绘制按钮和提示背景和文字* Draw buttons and tips for background and text** @param canvas*/protected void draw(Canvas canvas) {int offset = (int) (lineWidth * currPercent);canvas.save();canvas.translate(offset, 0);String text2Draw = "";int hintW = 0, hintH = 0;float[] result = getCurrentRange();if (mHideProgressHint) {isShowingHint = false;} else {if (isLeft) {if (mHintText2Draw == null) {text2Draw = (int) result[0] + "";} else {text2Draw = mHintText2Draw;}// if is the start,change the thumb colorisPrimary = (compareFloat(result[0], mMin) == 0);} else {if (mHintText2Draw == null) {text2Draw = (int) result[1] + "";} else {text2Draw = mHintText2Draw;}isPrimary = (compareFloat(result[1], mMax) == 0);}hintH = (int) mHintBGHeight - 20;//设置提示背景的高度hintW = (int) (mHintBGWith == 0 ? (mCursorPaint.measureText(text2Draw) + DEFAULT_PADDING_LEFT_AND_RIGHT): mHintBGWith);if (hintW < 1.5f * hintH) hintW = (int) (1.5f * hintH);}if (bmp != null) {canvas.drawBitmap(bmp, left, lineTop - bmp.getHeight() / 2, null);if (isShowingHint) {Rect rect = new Rect();rect.left = left - (hintW / 2 - bmp.getWidth() / 2);rect.top = bottom - hintH - bmp.getHeight();rect.right = rect.left + hintW;rect.bottom = rect.top + hintH - 10;drawNinePath(canvas, mProgressHintBG, rect);mCursorPaint.setColor(Color.WHITE);//提示字的颜色mCursorPaint.setTextSize(mTextSize);int x = (int) (left + (bmp.getWidth() / 2) - mCursorPaint.measureText(text2Draw) / 2);int y = bottom - hintH - bmp.getHeight() + hintH / 2;canvas.drawText(text2Draw, x, y, mCursorPaint);}} else {canvas.translate(left, 0);if (isShowingHint) {Rect rect = new Rect();rect.left = widthSize / 2 - hintW / 2;rect.top = defaultPaddingTop;rect.right = rect.left + hintW;rect.bottom = rect.top + hintH - 10;drawNinePath(canvas, mProgressHintBG, rect);mCursorPaint.setColor(Color.WHITE);mCursorPaint.setTextSize(mTextSize);int x = (int) (widthSize / 2 - mCursorPaint.measureText(text2Draw) / 2);// TODO: 2017/2/6//这里和背景形状有关,暂时根据本图形状比例计算//Here and the background shape, temporarily based on the shape of this figure ratio calculationint y = hintH / 3 + defaultPaddingTop + mCursorTextHeight / 2;canvas.drawText(text2Draw, x, y, mCursorPaint);}drawDefault(canvas);}canvas.restore();}/*** 绘制 9Path** @param c* @param bmp* @param rect*/public void drawNinePath(Canvas c, Bitmap bmp, Rect rect) {NinePatch patch = new NinePatch(bmp, bmp.getNinePatchChunk(), null);patch.draw(c, rect);}/*** 如果没有图片资源,则绘制默认按钮* <p>* If there is no image resource, draw the default button** @param canvas*/private void drawDefault(Canvas canvas) {int centerX = widthSize / 2;int centerY = lineBottom - mSeekBarHeight / 2;int radius = (int) (widthSize * DEFAULT_RADIUS);// draw shadowdefaultPaint.setStyle(Paint.Style.FILL);canvas.save();canvas.translate(0, radius * 0.25f);canvas.scale(1 + (0.1f * material), 1 + (0.1f * material), centerX, centerY);defaultPaint.setShader(shadowGradient);canvas.drawCircle(centerX, centerY, radius, defaultPaint);defaultPaint.setShader(null);canvas.restore();// draw bodydefaultPaint.setStyle(Paint.Style.FILL);if (isPrimary) {//if not set the color,it will use default colorif (colorPrimary == 0) {defaultPaint.setColor(te.evaluate(material, 0xFFFFFFFF, 0xFFE7E7E7));} else {defaultPaint.setColor(colorPrimary);}} else {if (colorSecondary == 0) {defaultPaint.setColor(te.evaluate(material, 0xFFFFFFFF, 0xFF0000));} else {defaultPaint.setColor(colorSecondary);}}canvas.drawCircle(centerX, centerY, radius, defaultPaint);// draw borderdefaultPaint.setStyle(Paint.Style.STROKE);defaultPaint.setColor(0xFFD7D7D7);canvas.drawCircle(centerX, centerY, radius, defaultPaint);}final TypeEvaluator<Integer> te = new TypeEvaluator<Integer>() {@Overridepublic Integer evaluate(float fraction, Integer startValue, Integer endValue) {int alpha = (int) (Color.alpha(startValue) + fraction * (Color.alpha(endValue) - Color.alpha(startValue)));int red = (int) (Color.red(startValue) + fraction * (Color.red(endValue) - Color.red(startValue)));int green = (int) (Color.green(startValue) + fraction * (Color.green(endValue) - Color.green(startValue)));int blue = (int) (Color.blue(startValue) + fraction * (Color.blue(endValue) - Color.blue(startValue)));return Color.argb(alpha, red, green, blue);}};/*** 拖动检测** @param event* @return*/protected boolean collide(MotionEvent event) {float x = event.getX();float y = event.getY();int offset = (int) (lineWidth * currPercent);return x > left + offset && x < right + offset && y > top && y < bottom;}private void slide(float percent) {if (percent < 0) percent = 0;else if (percent > 1) percent = 1;currPercent = percent;}private void materialRestore() {if (anim != null) anim.cancel();anim = ValueAnimator.ofFloat(material, 0);anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {material = (float) animation.getAnimatedValue();invalidate();}});anim.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {material = 0;invalidate();}});anim.start();}public void setProgressHint(String hint) {mHintText2Draw = hint;}}//*********************************** SeekBar ***********************************//public interface OnRangeChangedListener {void onRangeChanged(RangeSeekBar view, float min, float max, boolean isFromUser);}public void setOnRangeChangedListener(OnRangeChangedListener listener) {callback = listener;}public void setValue(float min, float max) {min = min + offsetValue;max = max + offsetValue;if (min < minValue) {throw new IllegalArgumentException("setValue() min < (preset min - offsetValue) . #min:" + min + " #preset min:" + minValue + " #offsetValue:" + offsetValue);}if (max > maxValue) {throw new IllegalArgumentException("setValue() max > (preset max - offsetValue) . #max:" + max + " #preset max:" + maxValue + " #offsetValue:" + offsetValue);}if (reserveCount > 1) {if ((min - minValue) % reserveCount != 0) {throw new IllegalArgumentException("setValue() (min - preset min) % reserveCount != 0 . #min:" + min + " #preset min:" + minValue + "#reserveCount:" + reserveCount + "#reserve:" + reserveValue);}if ((max - minValue) % reserveCount != 0) {throw new IllegalArgumentException("setValue() (max - preset min) % reserveCount != 0 . #max:" + max + " #preset min:" + minValue + "#reserveCount:" + reserveCount + "#reserve:" + reserveValue);}leftSB.currPercent = (min - minValue) / reserveCount * cellsPercent;if (mSeekBarMode == 2) {rightSB.currPercent = (max - minValue) / reserveCount * cellsPercent;}} else {leftSB.currPercent = (min - minValue) / (maxValue - minValue);if (mSeekBarMode == 2) {rightSB.currPercent = (max - minValue) / (maxValue - minValue);}}if (callback != null) {if (mSeekBarMode == 2) {callback.onRangeChanged(this, leftSB.currPercent, rightSB.currPercent, false);} else {callback.onRangeChanged(this, leftSB.currPercent, leftSB.currPercent, false);}}invalidate();}public void setValue(float value) {setValue(value, mMax);}public void setRange(float min, float max) {setRules(min, max, reserveCount, cellsCount);}public void setRules(float min, float max, float reserve, int cells) {if (max <= min) {throw new IllegalArgumentException("setRules() max must be greater than min ! #max:" + max + " #min:" + min);}mMax = max;mMin = min;if (min < 0) {offsetValue = 0 - min;min = min + offsetValue;max = max + offsetValue;}minValue = min;maxValue = max;if (reserve < 0) {throw new IllegalArgumentException("setRules() reserve must be greater than zero ! #reserve:" + reserve);}if (reserve >= max - min) {throw new IllegalArgumentException("setRules() reserve must be less than (max - min) ! #reserve:" + reserve + " #max - min:" + (max - min));}if (cells < 1) {throw new IllegalArgumentException("setRules() cells must be greater than 1 ! #cells:" + cells);}cellsCount = cells;cellsPercent = 1f / cellsCount;reserveValue = reserve;reservePercent = reserve / (max - min);reserveCount = (int) (reservePercent / cellsPercent + (reservePercent % cellsPercent != 0 ? 1 : 0));if (cellsCount > 1) {if (mSeekBarMode == 2) {if (leftSB.currPercent + cellsPercent * reserveCount <= 1 && leftSB.currPercent + cellsPercent * reserveCount > rightSB.currPercent) {rightSB.currPercent = leftSB.currPercent + cellsPercent * reserveCount;} else if (rightSB.currPercent - cellsPercent * reserveCount >= 0 && rightSB.currPercent - cellsPercent * reserveCount < leftSB.currPercent) {leftSB.currPercent = rightSB.currPercent - cellsPercent * reserveCount;}} else {if (1 - cellsPercent * reserveCount >= 0 && 1 - cellsPercent * reserveCount < leftSB.currPercent) {leftSB.currPercent = 1 - cellsPercent * reserveCount;}}} else {if (mSeekBarMode == 2) {if (leftSB.currPercent + reservePercent <= 1 && leftSB.currPercent + reservePercent > rightSB.currPercent) {rightSB.currPercent = leftSB.currPercent + reservePercent;} else if (rightSB.currPercent - reservePercent >= 0 && rightSB.currPercent - reservePercent < leftSB.currPercent) {leftSB.currPercent = rightSB.currPercent - reservePercent;}} else {if (1 - reservePercent >= 0 && 1 - reservePercent < leftSB.currPercent) {leftSB.currPercent = 1 - reservePercent;}}}invalidate();}public float getMax() {return mMax;}public float getMin() {return mMin;}public float[] getCurrentRange() {float range = maxValue - minValue;if (mSeekBarMode == 2) {return new float[]{-offsetValue + minValue + range * leftSB.currPercent,-offsetValue + minValue + range * rightSB.currPercent};} else {return new float[]{-offsetValue + minValue + range * leftSB.currPercent,-offsetValue + minValue + range * 1.0f};}}@Overridepublic void setEnabled(boolean enabled) {super.setEnabled(enabled);this.isEnable = enabled;}public void setProgressDescription(String progress) {if (leftSB != null) {leftSB.setProgressHint(progress);}if (rightSB != null) {rightSB.setProgressHint(progress);}}public void setLeftProgressDescription(String progress) {if (leftSB != null) {leftSB.setProgressHint(progress);}}public void setRightProgressDescription(String progress) {if (rightSB != null) {rightSB.setProgressHint(progress);}}@Overridepublic boolean onTouchEvent(MotionEvent event) {if (!isEnable) return true;switch (event.getAction()) {case MotionEvent.ACTION_DOWN:boolean touchResult = false;if (rightSB != null && rightSB.currPercent >= 1 && leftSB.collide(event)) {currTouch = leftSB;touchResult = true;} else if (rightSB != null && rightSB.collide(event)) {currTouch = rightSB;touchResult = true;} else if (leftSB.collide(event)) {currTouch = leftSB;touchResult = true;}//Intercept parent TouchEventif (getParent() != null) {getParent().requestDisallowInterceptTouchEvent(true);}return touchResult;case MotionEvent.ACTION_MOVE:float percent;float x = event.getX();currTouch.material = currTouch.material >= 1 ? 1 : currTouch.material + 0.1f;if (currTouch == leftSB) {if (cellsCount > 1) {if (x < lineLeft) {percent = 0;} else {percent = (x - lineLeft) * 1f / (lineWidth);}int touchLeftCellsValue = Math.round(percent / cellsPercent);int currRightCellsValue;if (mSeekBarMode == 2) {currRightCellsValue = Math.round(rightSB.currPercent / cellsPercent);} else {currRightCellsValue = Math.round(1.0f / cellsPercent);}percent = touchLeftCellsValue * cellsPercent;while (touchLeftCellsValue > currRightCellsValue - reserveCount) {touchLeftCellsValue--;if (touchLeftCellsValue < 0) break;percent = touchLeftCellsValue * cellsPercent;}} else {if (x < lineLeft) {percent = 0;} else {percent = (x - lineLeft) * 1f / (lineWidth);}if (mSeekBarMode == 2) {if (percent > rightSB.currPercent - reservePercent) {percent = rightSB.currPercent - reservePercent;}} else {if (percent > 1.0f - reservePercent) {percent = 1.0f - reservePercent;}}}leftSB.slide(percent);leftSB.isShowingHint = true;//Intercept parent TouchEventif (getParent() != null) {getParent().requestDisallowInterceptTouchEvent(true);}} else if (currTouch == rightSB) {if (cellsCount > 1) {if (x > lineRight) {percent = 1;} else {percent = (x - lineLeft) * 1f / (lineWidth);}int touchRightCellsValue = Math.round(percent / cellsPercent);int currLeftCellsValue = Math.round(leftSB.currPercent / cellsPercent);percent = touchRightCellsValue * cellsPercent;while (touchRightCellsValue < currLeftCellsValue + reserveCount) {touchRightCellsValue++;if (touchRightCellsValue > maxValue - minValue) break;percent = touchRightCellsValue * cellsPercent;}} else {if (x > lineRight) {percent = 1;} else {percent = (x - lineLeft) * 1f / (lineWidth);}if (percent < leftSB.currPercent + reservePercent) {percent = leftSB.currPercent + reservePercent;}}rightSB.slide(percent);rightSB.isShowingHint = true;}if (callback != null) {float[] result = getCurrentRange();callback.onRangeChanged(this, result[0], result[1], true);}invalidate();//Intercept parent TouchEventif (getParent() != null) {getParent().requestDisallowInterceptTouchEvent(true);}break;case MotionEvent.ACTION_CANCEL:if (mSeekBarMode == 2) {rightSB.isShowingHint = false;}leftSB.isShowingHint = false;if (callback != null) {float[] result = getCurrentRange();callback.onRangeChanged(this, result[0], result[1], false);}//Intercept parent TouchEventif (getParent() != null) {getParent().requestDisallowInterceptTouchEvent(true);}break;case MotionEvent.ACTION_UP:if (mSeekBarMode == 2) {rightSB.isShowingHint = false;}leftSB.isShowingHint = false;currTouch.materialRestore();if (callback != null) {float[] result = getCurrentRange();callback.onRangeChanged(this, result[0], result[1], false);}//Intercept parent TouchEventif (getParent() != null) {getParent().requestDisallowInterceptTouchEvent(true);}break;}return super.onTouchEvent(event);}@Overridepublic Parcelable onSaveInstanceState() {Parcelable superState = super.onSaveInstanceState();SavedState ss = new SavedState(superState);ss.minValue = minValue - offsetValue;ss.maxValue = maxValue - offsetValue;ss.reserveValue = reserveValue;ss.cellsCount = cellsCount;float[] results = getCurrentRange();ss.currSelectedMin = results[0];ss.currSelectedMax = results[1];return ss;}@Overridepublic void onRestoreInstanceState(Parcelable state) {SavedState ss = (SavedState) state;super.onRestoreInstanceState(ss.getSuperState());float min = ss.minValue;float max = ss.maxValue;float reserve = ss.reserveValue;int cells = ss.cellsCount;setRules(min, max, reserve, cells);float currSelectedMin = ss.currSelectedMin;float currSelectedMax = ss.currSelectedMax;setValue(currSelectedMin, currSelectedMax);}private class SavedState extends BaseSavedState {private float minValue;private float maxValue;private float reserveValue;private int cellsCount;private float currSelectedMin;private float currSelectedMax;SavedState(Parcelable superState) {super(superState);}private SavedState(Parcel in) {super(in);minValue = in.readFloat();maxValue = in.readFloat();reserveValue = in.readFloat();cellsCount = in.readInt();currSelectedMin = in.readFloat();currSelectedMax = in.readFloat();}@Overridepublic void writeToParcel(Parcel out, int flags) {super.writeToParcel(out, flags);out.writeFloat(minValue);out.writeFloat(maxValue);out.writeFloat(reserveValue);out.writeInt(cellsCount);out.writeFloat(currSelectedMin);out.writeFloat(currSelectedMax);}}/*** 根据手机的分辨率从 dp 的单位 转成为 px(像素)*/private int dp2px(Context context, float dpValue) {final float scale = context.getResources().getDisplayMetrics().density;return (int) (dpValue * scale + 0.5f);}/*** Compare the size of two floating point numbers** @param a* @param b* @return 1 is a > b* -1 is a < b* 0 is a == b*/private int compareFloat(float a, float b) {int ta = Math.round(a * 1000);int tb = Math.round(b * 1000);if (ta > tb) {return 1;} else if (ta < tb) {return -1;} else {return 0;}}
}
取拖动条的最大值代码块:
//从缓存中拿值float maxValue = SharePreferencesUtils.getFloat(context, "maxValue", 0);if (0 == maxValue) {mMax = t.getFloat(R.styleable.RangeSeekBar_max, 100);//最大取值} else {mMax = maxValue;}
这边的maxValue 值是从缓存中取出来的,如果在缓存中这个key(maxValue)为空的话,默认值为0,为0的时候从本地的配置文件arrays.xml中拿取默认的最大值(100),该文件放置在values目录下面,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<resources><string-array name="markArray"><item>0</item><item>10</item><item>20</item><item>30</item><item>50</item><item>100</item></string-array></resources>
还有一段就是获取刻度的array。
//从缓存中拿值CharSequence tempArray[] = SharePreferencesUtils.getStringSet(context, "textArray", null);if (tempArray == null || tempArray.length == 0) {mTextArray = t.getTextArray(R.styleable.RangeSeekBar_markTextArray);} else {mTextArray = tempArray;}
一样的 存储方式,放在SharePreferences内存当中,默认为null,默认获取的就是上面的arrays.xml文件。不为空则在本内存中获取,那么这边可能有人会问,SharePreferences怎么存储和获取数组元素,我这边贴出来一下:
package com.ds.platform.utils;import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;import com.ds.platform.bean.Lot;import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;/*** 覆盖模式的SharePreference*/
public class SharePreferencesUtils {private final static String SP_NAME = "sp_cache";private static SharedPreferences mPreferences; // SharedPreferences的实例private static final String TAG = LogUtils.LogName;private static SharedPreferences getSp(Context context) {if (mPreferences == null) {mPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);//覆盖}return mPreferences;}/*** 通过SP获得boolean类型的数据,没有默认为false** @param context : 上下文* @param key : 存储的key* @return*/public static boolean getBoolean(Context context, String key) {SharedPreferences sp = getSp(context);return sp.getBoolean(key, false);}/*** 通过SP获得boolean类型的数据,没有默认为false** @param context : 上下文* @param key : 存储的key* @param defValue : 默认值* @return*/public static boolean getBoolean(Context context, String key, boolean defValue) {SharedPreferences sp = getSp(context);return sp.getBoolean(key, defValue);}/*** 设置int的缓存数据** @param context* @param key :缓存对应的key* @param value :缓存对应的值*/public static void setBoolean(Context context, String key, boolean value) {SharedPreferences sp = getSp(context);SharedPreferences.Editor edit = sp.edit();// 获取编辑器edit.putBoolean(key, value);edit.commit();}/*** 缓存float** @param context* @param key* @param defValue* @return*/public static void setFloat(Context context, String key, float defValue) {SharedPreferences sp = getSp(context);SharedPreferences.Editor edit = sp.edit();// 获取编辑器edit.putFloat(key, defValue);edit.commit();}/*** 获取int型数据* @param context* @param key* @param defValue* @return*/public static int getInt(Context context, String key, int defValue) {SharedPreferences sp = getSp(context);return sp.getInt(key, defValue);}/*** 获取string值* @param context* @param key* @param defValue* @return*/public static String getString(Context context, String key, String defValue) {SharedPreferences sp = getSp(context);return sp.getString(key, defValue);}public static float getFloat(Context context, String key, float defValue) {SharedPreferences sp = getSp(context);return sp.getFloat(key, defValue);}public static long getLong(Context context, String key, long defValue) {SharedPreferences sp = getSp(context);return sp.getLong(key, 0);}/*** 储存数组*/public static void setArrayData(Context context, String key, CharSequence text[]) {if (text == null || text.length == 0) {return;}SharedPreferences sp = getSp(context);SharedPreferences.Editor edit = sp.edit();// 获取编辑器Set set = new HashSet();for (int i = 0; i < text.length; i++) {set.add(text[i]);}edit.putStringSet(key, set);edit.commit();}/*** 获取数组*/public static CharSequence[] getStringSet(Context context, String key, Set<String> defValue) {CharSequence sequence[] = new CharSequence[6];SharedPreferences sp = getSp(context);Set set = sp.getStringSet(key, defValue);if (set != null && set.size() > 0) {Iterator<Object> it = set.iterator();for (int i = 0; i < set.size(); i++) {sequence[i] = (CharSequence) it.next();}Arrays.sort(sequence);}return sequence;}/*** 保存list数据*/public static void setArrayList(Context context, String key, ArrayList<Lot> list) {if (list == null || list.size() == 0) {return;}Log.d(TAG, list.size() + "");SharedPreferences sp = getSp(context);SharedPreferences.Editor edit = sp.edit();// 获取编辑器int size = list.size();setInt(context, "listSize", size);//保存list的大小for (int i = 0; i < list.size(); i++) {edit.putString(key + i, list.get(i).getId());edit.putString(key + i + "n", list.get(i).getName());}edit.commit();}/*** 获取list数据*/public static ArrayList<Lot> getArrayList(Context context, String key) {ArrayList<Lot> list = new ArrayList<>();int size = getInt(context, "listSize", 0);for (int i = 0; i < size; i++) {Lot lot = new Lot();lot.setId(getString(context, key + i, null));lot.setName(getString(context, key + i + "n", null));list.add(lot);}return list;}//删除listpublic static void deleteList(Context context, String key) {SharedPreferences sp = getSp(context);SharedPreferences.Editor edit = sp.edit();// 获取编辑器int size = getInt(context, "listSize", 0);for (int i = 0; i < size; i++) {edit.remove(key + i);edit.remove(key + i + "n");edit.commit();}deleteData(context, "listSize");//删除列表玩法的缓存数据}/*** 设置int的缓存数据** @param context* @param key :缓存对应的key* @param value :缓存对应的值*/public static void setInt(Context context, String key, int value) {SharedPreferences sp = getSp(context);SharedPreferences.Editor edit = sp.edit();// 获取编辑器edit.putInt(key, value);edit.commit();}public static void setString(Context context, String key, String value) {SharedPreferences sp = getSp(context);SharedPreferences.Editor edit = sp.edit();// 获取编辑器edit.putString(key, value);edit.commit();}public static void setLong(Context context, String key, long value) {SharedPreferences sp = getSp(context);SharedPreferences.Editor edit = sp.edit();// 获取编辑器edit.putLong(key, value);edit.commit();}public static void setInt(Context context, String key, String value) {SharedPreferences sp = getSp(context);SharedPreferences.Editor edit = sp.edit();// 获取编辑器edit.putString(key, value);edit.commit();}/*** 删除指定key的value** @param context* @param key*/public static void deleteData(Context context, String key) {SharedPreferences sp = getSp(context);SharedPreferences.Editor edit = sp.edit();// 获取编辑器edit.remove(key);edit.commit();}/*** 清空缓存中的全部数据** @param context*/public static void clearData(Context context) {SharedPreferences sp = getSp(context);SharedPreferences.Editor editor = sp.edit();editor.clear().commit();}
}
上述是我项目中用到的,需要的可以看一下,另外分享一个小技巧,如果你想知道app中存入在Sp内存中的数据怎么看呢?可以在android studio中去查看,工具导航栏Tools–>Android–>Android Device Monitor ,打开之后点击右侧导航菜单的File Explorer ,找到data–>data–>自己的项目包名–>shared_prefs–>自己定义的sp名字,这边定义的是sp_cache,然后该目录下就存在一个sp_cache.xml的文件,点击右上角的图标 第一个导出电脑就可以查看了。好了,原归正传:
我怎么动态设置数组呢?先看下效果图:
不知你们有没有发现一些细节,每次拖动条上面的刻度文字跟跳转另一个activity输入框的数字是顺序大小一样,还有动态去设置刻度值得时候我没有从0开始,而是大于0的数字开始,我也做了启用停用的开关,停用之后不能使用拖动条功能,这个状态也是保存在SP内存里面的。
接下来看一下拖动条的对话框,我使用fragment做的,这边也一起说一下吧!
代码如下:
package com.ds.platform.dialog;import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;import com.ds.platform.R;
import com.ds.platform.activity.OrderActivity;
import com.ds.platform.utils.LogUtils;
import com.ds.platform.view.RangeSeekBar;/*** @author: Allen.* @date: 2017/4/14* @description: 拖动条对话框*/public class MyDialogFragment extends android.support.v4.app.DialogFragment {private RangeSeekBar seekbar1;private String progressData = "0";//初始第一个数值private static final String TAG = LogUtils.LogName;private OrderActivity orderActivity;@Nullable@Overridepublic View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {View view = inflater.inflate(R.layout.dialog_balance_seekbar, null);orderActivity = (OrderActivity) getActivity();initView(view);//初始控件//去除标题getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);setCancelable(true);return view;}/*** 获取控件** @param view*/private void initView(View view) {seekbar1 = (RangeSeekBar) view.findViewById(R.id.seekbar1);seekbar1.setValue(0);//设置默认值//监听控件去设置列表订单的item金额seekbar1.setOnRangeChangedListener(new RangeSeekBar.OnRangeChangedListener() {@Overridepublic void onRangeChanged(RangeSeekBar view, float min, float max, boolean isFromUser) {seekbar1.setProgressDescription((int) min + "");progressData = ((int) min) + "";orderActivity.setMoney(Integer.parseInt(progressData));}});}
}
那怎么使用这个fragment呢?首先不用思考的,其父类必须继承FragmentActivity,然后在父类的layout中必须有
<FrameLayout
android:id="@+id/fragment"android:layout_width="match_parent"android:layout_height="match_parent" />
然后可以通过view来触发这个fragment。
FragmentTransaction tran = getSupportFragmentManager().beginTransaction();MyDialogFragment dialogFragment = new MyDialogFragment();dialogFragment.show(tran, "myDialog");
需要注意的是要用V4的getSupportFragmentManager。
MyDialogFragment 的layout中需要使用自己定义的seekbar,如下:
<com.dsn.platform.view.RangeSeekBarandroid:id="@+id/seekbar1"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_centerVertical="true"android:layout_toRightOf="@+id/view1"app:cellMode="number"app:lineColorEdge="@color/silery"app:lineColorSelected="@color/main_top"app:markTextArray="@array/markArray"app:seekBarMode="single"app:textPadding="15dp"app:textSize="@dimen/text14"app:textColor="@color/login_text"app:seekBarResId="@drawable/seekbar_thumb" /></RelativeLayout>
该控件上的属性封装在attrs.xml中:
<?xml version="1.0" encoding="utf-8"?>
<resources><!-- ******************** 参数解释 ******************** --><!--最大值--><!--最小值--><!--两个按钮的最小间距--><!--cells 等于0为普通模式,大于1时切换为刻度模式--><!--是否关闭进度提示--><!--拖动后的Seekbar颜色--><!--默认的Seekbar颜色--><!--进度为最小值或最大值时按钮的颜色,默认此属性不调用--><!--进度不为最小值或最大值时按钮的颜色,默认此属性不调用--><!--刻度文字,不设置的时候默认隐藏--><!--按钮的背景资源,不设置的时候默认为圆形按钮--><!--进度提示背景资源,必须使用 9 path文件--><!--刻度文字与进度条之间的距离--><!--刻度文字和进度提示文字的大小--><!--进度提示背景的高度,不设置时根据文字尺寸自适应--><!--进度提示背景的宽度,不设置时根据文字尺寸自适应--><!--进度提示背景和进度条之间的距离--><!--进度条的高度--><!--按钮的尺寸--><!--刻度模式number 根据刻度的实际所占比例分配位置(markTextArray中必须都为数字)other 平分当前布局(markTextArray可以是任何字符)--><!--单向、双向模式 single 单向模式,只有一个按钮 range 双向模式,有两个按钮 --><!-- ******************** 参数解释 ******************** --><declare-styleable name="RangeSeekBar"><attr name="max" format="float"/><attr name="min" format="float"/><attr name="reserve" format="float"/><attr name="cells" format="integer"/><attr name="hideProgressHint" format="boolean"/><attr name="lineColorSelected" format="color"/><attr name="lineColorEdge" format="color"/><attr name="thumbPrimaryColor" format="color"/><attr name="thumbSecondaryColor" format="color"/><attr name="markTextArray" format="reference"/><attr name="seekBarResId" format="reference"/><!-- must use 9 path !!!--><attr name="progressHintResId" format="reference"/><attr name="textPadding" format="dimension" /><attr name="textSize" format="dimension" /><attr name="textColor" format="color" /><attr name="hintBGHeight" format="dimension" /><attr name="hintBGWith" format="dimension" /><attr name="hintBGPadding" format="dimension" /><attr name="seekBarHeight" format="dimension"/><attr name="thumbSize" format="dimension"/><attr name="cellMode" format="enum"><enum name="number" value="0"/><enum name="other" value="1"/></attr><attr name="seekBarMode" format="enum"><enum name="single" value="1"/><enum name="range" value="2"/></attr></declare-styleable>
</resources>
这边要注意progressHintResId属性,设置刻度提示背景必须使用.9的图片。
另一个跳转的activity页面我就不贴代码了,逻辑很简单,把输入的数字封装成数组,存入SP中,然后你每次弹出拖动条对话框,它都会去判断缓存中是否有值。
好了,今天先分享这个,感谢!
Android——自定义带刻度的SeekBar单向拖动条相关推荐
- android 自定义带刻度的seekbar,[Android开发]仿天天P图带气泡显示百分比进度的自定义SeekBar...
仿天天P图图像美化修改工具,素材来自于天天P图,效果图 效果就是点击之后会有气泡显示进度,优点是气泡不占用控件的高度 其他效果可参看https://github.com/AnderWeb/discre ...
- Android 基础知识4-3.8 SeekBar(拖动条)详解
一.简介 拖动条类似进度条,不同的是用户可以控制,比如,应用程序中用户可以对音效进行控制,这就可以使用拖动条来实现.由于拖动条可以被用户控制,所以需要对其进行事件监听,这就需要实现SeekBar.on ...
- android 自定义进度条 水量,Android自定义带水滴的进度条样式(带渐变色效果)...
一.直接看效果 二.直接上代码 1.自定义控件部分 package com.susan.project.myapplication; import android.app.Activity; impo ...
- android 带箭头的按钮,android自定义带箭头对话框
本文实例为大家分享了android自定义带箭头对话框的具体代码,供大家参考,具体内容如下 import android.content.context; import android.content. ...
- android音频声调,Android自定义带拼音音调Textview
本文实例为大家分享了Android自定义带拼音音调Textview的具体代码,供大家参考,具体内容如下 1.拼音textview,简单的为把拼音数组和汉字数组结合在一起多行显示 import andr ...
- 精通Android自定义View(十二)绘制圆形进度条
1 绘图基础简析 1 精通Android自定义View(一)View的绘制流程简述 2 精通Android自定义View(二)View绘制三部曲 3 精通Android自定义View(三)View绘制 ...
- android自定义带进度条的圆形图片
前言:在项目听新闻的改版中需要实现环绕圆形新闻图片的进度条功能,作为技术预备工作我就去看了一些网上的相关的原理,做了一个自定义带进度条的圆形图片的demo,并将这个实现写成文章发布出来,谁需要了可以进 ...
- Android 自定义收音机刻度
自定义收音机刻度:刻度可左右滑动 效果图 自定义View代码 import android.annotation.SuppressLint; import android.content.Contex ...
- Android 自定义带图标Toast,工具方法,Toast自定义显示时间
带图标Toast工具方法1 样式 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:an ...
最新文章
- 我校四名学生在全国中学生物理竞赛中勇夺一金三银并全部直保清华
- js(javascript)与OC(Objective-C)交互
- 集合中的遍历以及删除元素
- html多出的空白页怎么删除,word多出一页空白页怎么删除,这4个方法总有一个能解决,真实挂机网赚项目...
- ONNX系列二 --- 使用ONNX使Keras模型可移植
- linux java占用199%,linux分区使用率过高又查询不到被哪些文件占用的问题
- HDUOJ1043Eight 八数码问题可以构造解
- mysql安装时1045错误_MySql 安装时的1045错误
- pytest框架+conftest.py配置公共数据的准备和清理
- wget 下载百度网盘文件
- EMI/EMC设计经典问答
- 码农辞职一年后:独立工程师太难了
- 如何在 Windows 10 上完全禁用 UAC
- netbsd apache php mysql,NetBSD配置aria2的web前端YAAW笔记
- ISA防火墙规则练习
- 用IOC和DI解决懒人老板想喝咖啡但不想自己动手的窘迫
- Linux、git和github的故事
- windows的命令行工具和DOS工具的区别
- 【基础】一叶知秋,从背包问题到动态规划
- zblog php换域名,zblog 怎么更换域名