Android implements addition and subtraction custom controls

About the author: Xiaobei Programming (focusing on HarmonyOS, Android, Java, Web, TCP/IP and other technical directions)
Blog homepage: Open Source China, Rare Earth Nuggets, 51cto Blog, Blog Park, Zhihu, Jianshu, MOOC, CSDN
If the article is helpful to you, please follow, like, collect, and comment.
If you need to reprint, please refer to [Reprint Instructions]

Introduction

This article will introduce a custom CounterView control and show how to create a customizable numeric input control that can set the minimum value, maximum value, step value, and other properties.

Control structure

The CounterView control consists of the following elements:
Decrease button (decreaseButton): used to decrement the value.
Increase button (increaseButton): used to increment the value.
Value edit box (valueEditText): used to display and edit values.
Custom View (customView): used to display custom content (if needed).

Control properties

The CounterView control has the following properties:
Minimum value (minValue): The minimum allowed value of the control value.
Maximum value (maxValue): The maximum allowed value of the control value.
Increment: Controls the step size when increasing or decreasing.
Default value (defaultValue): the initial value of the control.
Decimal place control (decimalEnabled and decimalPlaces): Control whether decimals are allowed to be entered and the limit of decimal places.

Control functions

The CounterView control has the following functions:
Supports increment and decrement operations.
Limits the range of input values to between the minimum and maximum values.
Supports decimal input and can set the number of decimal places as needed.
Provides a listener for value changes, which can perform corresponding operations when the value changes.
main method
The following is a brief description of some important methods in the CounterView control:

Method Function
init Initialize each element of the control, including buttons, edit boxes and custom views
applyAttributes Used to apply attributes passed from XML layout
setDefaultValue Set the default value of the control
setMinValue Set the minimum value
setMaxValue Set the maximum value limit
setIncrement Set increment or decrement The step size
setValue Set the value displayed by the control
decreaseValue Decrease the value and limit and prompt according to the set range
increaseValue Increase the value, and limit and prompt according to the set range
setDecimalEnabled Used to enable or disable decimal input and limit it according to the set number of decimal places
applyDecimalFilter Apply a decimal filter to ensure that the entered value meets the set decimal place requirements
notifyValueChanged Notify the listener when the value changes
setOnValueChangedListener Set a listener for value changes to perform corresponding operations when the value changes
The control code can be modified according to your needs
import android.content.Context;
import android.icu.math.BigDecimal;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputType;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;

/**
 * Copyright (C) 2023-2024 Author
 *
 * Quantity addition and subtraction controls
 * Enter the minus sign in front and the plus sign in the middle
 *
 * @author xiaolu
 * @date 2023/11/8
 * @version 1.0.0
 */
public class CounterView extends LinearLayout {
    private Button decreaseButton; // decrease button
    private Button increaseButton; // Add button
    private EditText valueEditText; // Numeric edit box
    private View customView; // Custom view

    private BigDecimal minValue = BigDecimal.ZERO; // minimum value
    private BigDecimal maxValue = BigDecimal.valueOf(100); // maximum value
    private BigDecimal increment = BigDecimal.ONE; // step value
    private BigDecimal defaultValue = BigDecimal.ZERO; //Default value
    private OnValueChangedListener valueChangedListener; // Value change listener
    private boolean decimalEnabled = false; // Whether to allow decimals
    private int decimalPlaces = 0; // number of decimal places

    /**
     * Constructor, used to create CustomCounterView instances.
     *
     * @param context context parameter, cannot be empty
     */
    public CounterView(Context context) {
        super(context);
        init(context);
    }

    /**
     * Constructor, used to create CustomCounterView instances.
     *
     * @param context context parameter, cannot be empty
     * @param attrs attribute parameters
     */
    public CounterView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
        applyAttributes(context, attrs);
    }

    /**
     * Constructor, used to create CustomCounterView instances.
     *
     * @param context context parameter, cannot be empty
     * @param attrs attribute parameters
     * @param defStyle default style parameters
     */
    public CounterView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
        applyAttributes(context, attrs);
    }

    //Initialization method
    private void init(Context context) {
        decreaseButton = new Button(context);
        decreaseButton.setText("-");
        addView(decreaseButton);

        valueEditText = new EditText(context);
        valueEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
        addView(valueEditText);

        increaseButton = new Button(context);
        increaseButton.setText(" + ");
        addView(increaseButton);

        customView = new View(context);
        addView(customView);

        decreaseButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                decreaseValue();
            }
        });

        increaseButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                increaseValue();
            }
        });

        valueEditText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                try {
                    BigDecimal currentValue = new BigDecimal(s.toString());
                    if (currentValue.compareTo(minValue) < 0) {
                        ToastUtil.showShort("The input number cannot be less than the minimum value" + minValue);
                        setEditTextValue(minValue.toString());
                    } else if (currentValue.compareTo(maxValue) > 0) {
                        ToastUtil.showShort("The input number cannot be greater than the maximum value " + maxValue);
                        setEditTextValue(maxValue.toString());
                    }
                } catch (NumberFormatException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });
    }

    //Apply attribute method
    private void applyAttributes(Context context, AttributeSet attrs) {

    }

    // Set default value method
    public void setDefaultValue(BigDecimal defaultValue) {
        this.defaultValue = defaultValue;
        setValue(defaultValue);
    }

    //Set the minimum value method
    public void setMinValue(BigDecimal minValue) {
        this.minValue = minValue;
    }

    //Set the maximum value method
    public void setMaxValue(BigDecimal maxValue) {
        this.maxValue = maxValue;
    }

    // Set step value method
    public void setIncrement(BigDecimal increment) {
        this.increment = increment;
    }

    // Get the current value method
    public BigDecimal getValue() {
        return BigDecimal.ZERO;
    }

    //Set numerical method
    public void setValue(BigDecimal value) {
        valueEditText.setText(value.toString());
        // Move the cursor to the end of the text
        valueEditText.setSelection(valueEditText.getText().length());
    }

    //Update text data method
    private void setEditTextValue(String value) {
        valueEditText.setText(value);
        // Move the cursor to the end of the text
        valueEditText.setSelection(valueEditText.getText().length());
    }

    // Reduce numerical method
    private void decreaseValue() {
        try {
            String input = valueEditText.getText().toString().trim();
            if (!input.isEmpty()) {
                BigDecimal currentValue = new BigDecimal(input);
                BigDecimal newValue = currentValue.subtract(increment);
                if (newValue.compareTo(minValue) < 0) {
                    ToastUtil.showShort("The input number cannot be less than the minimum value" + minValue);
                    newValue = minValue;
                }
                setEditTextValue(newValue.toString());
                notifyValueChanged();
            }
        } catch (NumberFormatException e) {
            setEditTextValue("0");
            notifyValueChanged();
        }
    }

    // Add numerical method
    private void increaseValue() {
        try {
            String input = valueEditText.getText().toString().trim();
            if (!input.isEmpty()) {
                BigDecimal currentValue = new BigDecimal(input);
                BigDecimal newValue = currentValue.add(increment);
                if (newValue.compareTo(maxValue) > 0) {
                    ToastUtil.showShort("The input number cannot be greater than the maximum value" + maxValue);
                    newValue = maxValue;
                }
                setEditTextValue(newValue.toString());
                notifyValueChanged();
            }
        } catch (NumberFormatException e) {
            setEditTextValue("0");
            notifyValueChanged();
        }
    }

    /**
     * Enable decimal input
     * @param decimalEnabled Whether to enable on: true off: false
     * @param decimalPlaces The number of decimal places is controlled. If it is -1, the number of decimal places is not controlled. Otherwise, it is the number of decimal places.
     */
    public void setDecimalEnabled(boolean decimalEnabled, int decimalPlaces) {
        this.decimalEnabled = decimalEnabled;
        this.decimalPlaces = decimalPlaces;
        if (decimalEnabled) {
            if (decimalPlaces == -1) {
                valueEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
            } else {
                valueEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED);
            }
        } else {
            valueEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
        }
        applyDecimalFilter();
    }

    private void applyDecimalFilter() {
        valueEditText.setFilters(new InputFilter[]{new InputFilter() {
            @Override
            public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
                if (decimalEnabled) {
                    String value = dest.toString().substring(0, dstart) + source.toString() + dest.toString().substring(dend);
                    if (!value.isEmpty() & amp; & amp; !value.equals(".") & amp; & amp; !value.equals("-")) {
                        try {
                            BigDecimal newValue = new BigDecimal(value);
                            if (decimalPlaces >= 0 & amp; & amp; newValue.scale() > decimalPlaces) {
                                return "";
                            }
                        } catch (NumberFormatException | ArithmeticException e) {
                            return "";
                        }
                    }
                }
                return null;
            }
        }});
    }

    // Notification value has changed method
    private void notifyValueChanged() {
        if (valueChangedListener != null) {
            valueChangedListener.onValueChanged(new BigDecimal(valueEditText.getText().toString()));
        }
    }

    //Set the value change listener method
    public void setOnValueChangedListener(OnValueChangedListener listener) {
        this.valueChangedListener = listener;
    }

    // Numeric change listener interface
    public interface OnValueChangedListener {
        void onValueChanged(BigDecimal newValue);
    }
}

xml reference
 <your package name.CounterView
        android:id="@ + id/counterView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

Set in code
 binding.counterView.setMinValue(BigDecimal.ZERO);
        binding.counterView.setMaxValue(BigDecimal.TEN);
        binding.counterView.setIncrement(BigDecimal.ONE);
        binding.counterView.setDefaultValue(BigDecimal.ZERO);
        binding.counterView.setDecimalEnabled(true,2);

        binding.counterView.setOnValueChangedListener(new CounterView.OnValueChangedListener() {
            @Override
            public void onValueChanged(BigDecimal newValue) {
                //Handle value change events here
                Logger.d("CounterViewActivity CounterView onValueChanged data change " + newValue.toString());
            }
        });

This article introduces the basic structure, functions and usage of the CounterView control, hoping to be helpful to developers when creating customized numeric input controls. By flexibly applying these methods and functions, they can be customized and adjusted according to specific application needs to provide users with a better application experience.

No matter what stage you are at, persistence is the key to success. Don’t stop and keep moving forward. Even if the road ahead is rough, stay optimistic and courageous. Believe in your abilities and the goals you pursue will be achieved in the near future. come on!