前兩天迭代一個報價的APP,選擇商品進行結算價格。增加一個人性化操作,長按控件進行快速增長數值。
根據控件上個Gif圖:
除了可以自定義各種顏色、大小等屬性之外,可以進行單點、長按、滑動改變數值。
原理是監聽其觸摸事件進行相應判斷操作,觸摸結束進行動畫回覆操作。具體的已經在代碼中註釋。
1.自定義StepperView 控件
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AccelerateInterpolator;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.zachary.util.R;
import java.lang.ref.WeakReference;
/**
* Created by zachary on 19/6/14.
*/
public class StepperView extends RelativeLayout implements View.OnTouchListener, ValueAnimator.AnimatorListener, ValueAnimator.AnimatorUpdateListener {
// 監聽
private StepperValueChangeListener listener;
// 滑塊數值
private TextView tvStepperContent;
// 加減
private ImageView ivStepperMinus, ivStepperPlus;
// 恢復動畫時間
public static int ANIMATIONDURATION = 300;
// 動畫中,不能進行滑動
public boolean animationing = false;
private UpdateRunnable updateRunnable;
// 是否按着,判斷是否還要繼續更新數值和界面
private boolean stepTouch = false;
// 按下後多少間隔觸發快速改變模式
private static final long STEPSPEEDCHANGEDURATION = 1000;
// 數值更新頻率-慢
private static long UPDATEDURATIONSLOW = 300;
// 數值更新頻率-快
private static long UPDATEDURATIONFAST = 100;
// 慢速遞增值 步長
private int valueSlowStep = 1;
// 按下的初始x值
private float startX = 0;
// 滑塊左側的座標
private float startStepperContentLeft = 0;
private boolean hasStepperContentLeft = false;
// 按下時間
private long startTime = 0;
// 當前狀態
private int status = STATUS_NORMAL;
private static final int STATUS_MIMNUS = -1;
private static final int STATUS_PLUS = 1;
private static final int STATUS_NORMAL = 0;
// 當前模式
private Mode mode = Mode.AUTO;
public enum Mode {
AUTO(0), CUSTOM(1);
private final int value;
Mode(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static Mode valueOf(int value) { // 手寫的從int到enum的轉換函數
switch (value) {
case 0:
return AUTO;
case 1:
return CUSTOM;
}
return AUTO;
}
}
// 默認數值
private int value = 0;
private int minValue = 0;
private int maxValue = 200;
public StepperView(Context context) {
this(context, null);
}
public StepperView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化佈局
initViews(attrs);
}
// 初始化
private void initViews(AttributeSet attrs) {
LayoutInflater.from(getContext()).inflate(R.layout.view_stepper, this, true);
tvStepperContent = findViewById(R.id.tvStepperContent);
ivStepperMinus = findViewById(R.id.ivStepperMinus);
ivStepperPlus = findViewById(R.id.ivStepperPlus);
String text = "";
Drawable background = null;
Drawable contentBackground = null;
Drawable leftButtonResources = null;
Drawable rightButtonResources = null;
Drawable leftButtonBackground = null;
Drawable rightButtonBackground = null;
int contentTextColor = getResources().getColor(R.color.cl_text);
float contentTextSize = 0;
if (attrs != null) {
TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.StepperView);
// 類型
int modeValue = array.getInt(R.styleable.StepperView_mode, Mode.AUTO.getValue());
mode = Mode.valueOf(modeValue);
// 最小值
minValue = array.getInt(R.styleable.StepperView_min, minValue);
// 最大值
maxValue = array.getInt(R.styleable.StepperView_max, maxValue);
// 當前值
value = valueRangeCheck(array.getInt(R.styleable.StepperView_value, value));
// 增加的步數
valueSlowStep = array.getInt(R.styleable.StepperView_step, valueSlowStep);
if (valueSlowStep <= 0) {
valueSlowStep = 1;
}
// 滑塊上的文字
text = array.getString(R.styleable.StepperView_text);
// 背景
background = array.getDrawable(R.styleable.StepperView_stepper_background);
contentBackground = array.getDrawable(R.styleable.StepperView_stepper_contentBackground);
leftButtonResources = array.getDrawable(R.styleable.StepperView_stepper_leftButtonResources);
rightButtonResources = array.getDrawable(R.styleable.StepperView_stepper_rightButtonResources);
leftButtonBackground = array.getDrawable(R.styleable.StepperView_stepper_leftButtonBackground);
rightButtonBackground = array.getDrawable(R.styleable.StepperView_stepper_rightButtonBackground);
contentTextColor = array.getColor(R.styleable.StepperView_stepper_contentTextColor, contentTextColor);
contentTextSize = array.getFloat(R.styleable.StepperView_stepper_contentTextSize, 0);
// 回收
array.recycle();
}
// 設置View的背景
if (background != null) {
setBackgroundDrawable(background);
} else {
setBackgroundResource(R.color.cl_btn_press);
}
// 設置中間內容滑條顏色
if (contentBackground != null) {
setContentBackground(contentBackground);
}
// 滑塊文字顏色
tvStepperContent.setTextColor(contentTextColor);
// 滑塊文字大小
if (contentTextSize > 0)
setContentTextSize(contentTextSize);
// 背景顏色
if (leftButtonBackground != null) {
ivStepperMinus.setBackgroundDrawable(leftButtonBackground);
}
if (rightButtonBackground != null) {
ivStepperPlus.setBackgroundDrawable(rightButtonBackground);
}
// 背景圖片
if (leftButtonResources != null) {
setLeftButtonResources(leftButtonResources);
}
if (rightButtonResources != null) {
setRightButtonResources(rightButtonResources);
}
// AUTO模式,寫數值到滑動條上
if (mode == Mode.AUTO)
tvStepperContent.setText(String.valueOf(value));
else
tvStepperContent.setText(text);
// 設置後 onclick 產生的點擊狀態會失效,通過觸摸
ivStepperMinus.setOnTouchListener(this);
ivStepperPlus.setOnTouchListener(this);
setOnTouchListener(this);
updateRunnable = new UpdateRunnable(this);
}
// 觸摸事件,根據View判斷:長按操作或者滑動操作
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// view觸摸中
stepTouch = true;
// 慢更新
postDelayed(updateRunnable, UPDATEDURATIONSLOW);
// 針對非按鈕則記錄位置
startX = event.getX();
// 只需要獲取一次:初始左邊距離
initStartStepperContentLeft();
// 記錄開始時間
startTime = System.currentTimeMillis();
// 如果是兩邊的按鈕,分別設置爲點擊狀態
if (v == ivStepperMinus) {
ivStepperMinus.setPressed(true);
// 記錄觸摸狀態
status = STATUS_MIMNUS;
break;
} else if (v == ivStepperPlus) {
ivStepperPlus.setPressed(true);
status = STATUS_PLUS;
break;
}
break;
case MotionEvent.ACTION_MOVE:
// 是按鈕則不能移動,恢復位置的動畫中也不能移動
if (v == ivStepperMinus || v == ivStepperPlus || animationing) break;
// 非按鈕則進行移動
float moveX = event.getX() - startX;
// 移動後的x座標
float x = moveX + startStepperContentLeft;
// 設置當前滑動的滑塊位置
moveStepperContent(x);
// 判斷滑動狀態,數值改變
moveEffectStatus(moveX);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
stepTouch = false;
// 如果是兩邊的按鈕,分別設置爲點擊狀態
if (v == ivStepperMinus) {
ivStepperMinus.setPressed(false);
break;
} else if (v == ivStepperPlus) {
ivStepperPlus.setPressed(false);
break;
}
// 滑塊回覆
restoreStepperContent();
break;
}
return true;
}
private void initStartStepperContentLeft() {
if (hasStepperContentLeft) return;
hasStepperContentLeft = true;
//開始狀態時left距離
startStepperContentLeft = tvStepperContent.getLeft();
}
/**
* 中間滑條恢復原位置
*/
private void restoreStepperContent() {
if (animationing) return;
animationing = true;
ValueAnimator restoreTranslateAnimation = ValueAnimator.ofFloat(tvStepperContent.getLeft(), (int) startStepperContentLeft);
restoreTranslateAnimation.setDuration(ANIMATIONDURATION);
restoreTranslateAnimation.addListener(this);
restoreTranslateAnimation.addUpdateListener(this);
restoreTranslateAnimation.setInterpolator(new AccelerateInterpolator());
restoreTranslateAnimation.start();
}
/**
* 移動位置
*
* @param x
*/
private void moveStepperContent(float x) {
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
//
params.leftMargin = (int) x;
// 限制子控件移動必須在視圖範圍內
// 最小爲零,最大爲兩個左邊空白
if (params.leftMargin < 0 || (params.leftMargin + tvStepperContent.getWidth()) > getWidth())
return;
params.topMargin = 0;
// 寬高不變
params.width = tvStepperContent.getWidth();
params.height = tvStepperContent.getHeight();
// 滑塊屬性設置
tvStepperContent.setLayoutParams(params);
}
/**
* 滑動狀態,判斷加減操作
*
* @param x
*/
private void moveEffectStatus(float x) {
// 觸發移動事件的最小距離,判斷用戶是否真的存在move
int scaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
if (x > scaledTouchSlop) {
// 正爲右:加數
status = STATUS_PLUS;
} else if (x < -scaledTouchSlop) {
// 負爲左:減數
status = STATUS_MIMNUS;
} else {
// 正常
status = STATUS_NORMAL;
}
}
/**
* 獲取變化後的值
*
* @return
*/
private int getNextValue() {
switch (status) {
case STATUS_MIMNUS:
return value - valueSlowStep;
case STATUS_PLUS:
return value + valueSlowStep;
case STATUS_NORMAL:
return value;
}
return value;
}
//回調更新UI顯示
private void updateUI() {
// 更新變化數值
int nextValue = getNextValue();
// 判斷是否在範圍之內
if (nextValue < minValue) {
nextValue = minValue;
}
if (nextValue > maxValue) {
nextValue = maxValue;
}
value = nextValue;
// AUTO模式,寫數值到滑動條上
if (mode == Mode.AUTO) {
tvStepperContent.setText(String.valueOf(value));
}
if (listener != null)
listener.onValueChange(this, value);
// 觸摸中
if (stepTouch) {
// 更新UI,先慢、後快
postDelayed(updateRunnable, (System.currentTimeMillis() - startTime > STEPSPEEDCHANGEDURATION) ? UPDATEDURATIONFAST : UPDATEDURATIONSLOW);
}
}
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Float value = (Float) animation.getAnimatedValue();
// 更新滑塊位置
moveStepperContent(value);
}
@Override
public void onAnimationEnd(Animator animation) {
animationing = false;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
//更新UI顯示
static class UpdateRunnable implements Runnable {
private WeakReference<StepperView> view;
public UpdateRunnable(StepperView view) {
this.view = new WeakReference<StepperView>(view);
}
public void run() {
StepperView stepper = view.get();
if (stepper != null) {
stepper.updateUI();
}
}
}
public void setOnValueChangeListener(StepperValueChangeListener listener) {
this.listener = listener;
}
/////////////////// 以下控件屬性設置 ///////////////////
/**
* 返回當前模式類型
*
* @return
*/
public Mode getMode() {
return mode;
}
/**
* 模式設置 AUTO(0) 數值寫到滑動條, CUSTOM(1) 自定義文字;
*
* @param mode
*/
public void setMode(Mode mode) {
this.mode = mode;
}
/**
* 獲取當前值
*
* @return
*/
public int getValue() {
return value;
}
/**
* 設置當前值
*
* @param value
*/
public void setValue(int value) {
this.value = valueRangeCheck(value);
if (mode == Mode.AUTO)//AUTO模式,寫數值到滑動條上
tvStepperContent.setText(String.valueOf(value));
}
/**
* 檢測數值的範圍
*
* @param value
* @return
*/
public int valueRangeCheck(int value) {
if (value > maxValue) value = maxValue;
else if (value < minValue) value = minValue;
return value;
}
/**
* 獲取最小值
*
* @return
*/
public int getMinValue() {
return minValue;
}
/**
* 設置最小值
*
* @return
*/
public void setMinValue(int minValue) {
this.minValue = minValue;
}
/**
* 獲取最大值
*
* @return
*/
public int getMaxValue() {
return maxValue;
}
/**
* 設置最大值
*
* @return
*/
public void setMaxValue(int maxValue) {
this.maxValue = maxValue;
}
/**
* 獲取步長
*
* @return
*/
public int getValueSlowStep() {
return valueSlowStep;
}
/**
* 設置步長
*
* @return
*/
public void setValueSlowStep(int valueSlowStep) {
this.valueSlowStep = valueSlowStep;
}
/**
* 設置中間內容滑條顏色
*
* @param resId
*/
public void setContentBackground(int resId) {
tvStepperContent.setBackgroundResource(resId);
}
public void setContentBackground(Drawable drawable) {
tvStepperContent.setBackgroundDrawable(drawable);
}
/**
* 設置中間內容文字顏色
*
* @param resId
*/
public void setContentTextColor(int resId) {
tvStepperContent.setTextColor(getResources().getColor(resId));
}
/**
* 設置中間內容文字,mode需爲Custom才支持
*
* @param text
*/
public void setText(String text) {
tvStepperContent.setText(text);
}
/**
* 設置中間內容文字大小
*
* @param size
*/
public void setContentTextSize(float size) {
tvStepperContent.setTextSize(size);
}
/**
* 設置按鈕背景
*
* @param resId
*/
public void setButtonBackGround(int resId) {
ivStepperMinus.setBackgroundResource(resId);
ivStepperPlus.setBackgroundResource(resId);
}
/**
* 設置按鈕資源
*
* @param resId
*/
public void setLeftButtonResources(int resId) {
ivStepperMinus.setImageResource(resId);
}
/**
* 設置按鈕資源
*
* @param drawable
*/
public void setLeftButtonResources(Drawable drawable) {
ivStepperMinus.setImageDrawable(drawable);
}
/**
* 設置按鈕資源
*
* @param resId
*/
public void setRightButtonResources(int resId) {
ivStepperPlus.setImageResource(resId);
}
/**
* 設置按鈕資源
*
* @param drawable
*/
public void setRightButtonResources(Drawable drawable) {
ivStepperPlus.setImageDrawable(drawable);
}
}
2.在attrs中:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="StepperView">
<attr name="min" format="integer"/>
<attr name="max" format="integer"/>
<attr name="value" format="integer"/>
<attr name="step" format="integer"/>
<attr name="text" format="string"/>
<attr name="mode" format="enum">
<enum name="auto" value="0"/>
<enum name="custom" value="1"/>
</attr>
<attr name="stepper_background" format="color|reference"/>
<attr name="stepper_buttonBackground" format="color|reference"/>
<attr name="stepper_contentBackground" format="color|reference"/>
<attr name="stepper_contentTextColor" format="color"/>
<attr name="stepper_contentTextSize" format="float"/>
<attr name="stepper_leftButtonBackground" format="color|reference"/>
<attr name="stepper_rightButtonBackground" format="color|reference"/>
<attr name="stepper_leftButtonResources" format="color|reference"/>
<attr name="stepper_rightButtonResources" format="color|reference"/>
</declare-styleable>
</resources>
3.佈局文件:
view_stepper.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:id="@+id/ivStepperMinus"
android:layout_width="30dp"
android:layout_height="match_parent"
android:background="@drawable/sl_stepper_button"
android:clickable="true"
android:padding="10dp"
android:scaleType="centerInside"
android:src="@drawable/ic_stepper_minus" />
<ImageView
android:id="@+id/ivStepperPlus"
android:layout_width="30dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:background="@drawable/sl_stepper_button"
android:clickable="true"
android:padding="10dp"
android:scaleType="centerInside"
android:src="@drawable/ic_stepper_plus" />
<TextView
android:id="@+id/tvStepperContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toLeftOf="@+id/ivStepperPlus"
android:layout_toRightOf="@+id/ivStepperMinus"
android:background="#2b8ccd"
android:gravity="center"
android:textColor="#000000"
android:textSize="15sp"
tools:text="100" />
</RelativeLayout>
4.其他文件
colors:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorTheme">#7ce0d3</color>
<color name="colorPrimaryDark">#7cecd3</color>
<color name="colorAccent">#FF4081</color>
<color name="cl_btn_normal">#3299cc</color>
<color name="cl_btn_press">#007fff</color>
<color name="cl_text_bg">#3299cc</color>
<color name="cl_text">#ffffff</color>
</resources>
selector:sl_stepper_button.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@color/cl_btn_press"/>
<item android:drawable="@color/cl_btn_normal"/>
</selector>
5.佈局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:padding="15dp"
android:gravity="center_vertical"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true">
<TextView
android:id="@+id/tvValue"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="30dp"
android:textSize="@dimen/dp_40"/>
<com.zachary.util.SnappingStepper.StepperView
android:id="@+id/stepper"
android:layout_width="120dp"
android:layout_height="30dp"
app:text="你好"/>
</LinearLayout>
</RelativeLayout>
6.Activity中的代碼
public class MainActivity extends Activity implements StepperValueChangeListener {
// 購物控件
StepperView stepper;
TextView tvValue;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initView();
}
//初始化佈局
private void initView() {
stepper = findViewById(R.id.stepper);
tvValue = findViewById(R.id.tvValue);
tvValue.setText(String.valueOf(stepper.getValue()));
stepper.setOnValueChangeListener(this);
}
@Override
public void onValueChange(View view, int value) {
switch (view.getId()){
case R.id.stepper:
tvValue.setText(String.valueOf(value));
break;
}
}
}
以上覆制可以直接打到圖上的效果,代碼不是很複雜,根據註釋完全可以看懂。