Android之自定義View:長按加減

前兩天迭代一個報價的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;
        }
    }

}

以上覆制可以直接打到圖上的效果,代碼不是很複雜,根據註釋完全可以看懂。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章