Search code examples
androidprogress-bar

How to constraint a view (TextView) to a progressBar current progress?


I have a project I'm working on, in this project I'm using progressBars as a chart of bars,

that is: I get a list of items, then I check which item has the max value and I set that max value to be the max progress of all the progressbars(items),

then I would like to show each of the items value (progress), but I want to attach it to the progress of the specific item, in other words I would like to show the progress value at the end of each progress of each progressBar,

Example: max progress 29, dont mind the percent value or the labels below

a busy cat
(source: datasciencecentral.com)

I've managed to make the progressbars vertical after some searching, but this issue I'm struggling to resolve,

Is there a way to attach something to the progress of a progress bar? is there a way to reference the progress in xml? (The max value and the different items progress is only known at runtime, and they remain static after)

thx.


Solution

  • Implementing this using the normal progress bar and making it vertical makes everything too complicated. So my solution is to create a custom view which consists of a view with a ClipDrawable background for progress and a TextView for label.

    Full code:

    LabeledVerticalProgressBar.java

    import android.content.Context;
    import android.content.res.ColorStateList;
    import android.content.res.TypedArray;
    import android.graphics.Typeface;
    import android.graphics.drawable.ClipDrawable;
    import android.graphics.drawable.ColorDrawable;
    import android.graphics.drawable.Drawable;
    import android.util.AttributeSet;
    import android.util.TypedValue;
    import android.view.Gravity;
    import android.view.View;
    import android.widget.TextView;
    
    import androidx.constraintlayout.widget.ConstraintLayout;
    
    import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
    import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
    import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID;
    
    public class LabeledVerticalProgressBar extends ConstraintLayout {
        private TextView textView;
        private View progressView;
    
        private Drawable progressDrawable;
        private float min;
        private float max;
        private float progress;
        private int labelTextAppearanceId;
        private boolean isLabelAbove = true;
        private int numDecimals;
        private String unit = "";
    
        private int height;
    
        public LabeledVerticalProgressBar(Context context, AttributeSet attrs) {
            super(context, attrs);
            init(attrs);
        }
    
        private void init(AttributeSet attrs) {
            initViews();
            initAttributes(attrs);
            initProgress();
            initLabel();
        }
    
        private void initViews() {
            progressView = new View(getContext());
            progressView.setId(View.generateViewId());
            LayoutParams progressParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT);
            progressParams.topToTop = PARENT_ID;
            progressParams.bottomToBottom = PARENT_ID;
            progressParams.startToStart = PARENT_ID;
            progressParams.endToEnd = PARENT_ID;
            progressView.setLayoutParams(progressParams);
    
            textView = new TextView(getContext());
            int padding = (int)(4 * getResources().getDisplayMetrics().density);
            textView.setPadding(padding, padding, padding, padding);
            LayoutParams textParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
            textParams.startToStart = progressView.getId();
            textParams.endToEnd = progressView.getId();
            textParams.bottomToBottom = progressView.getId();
            textView.setLayoutParams(textParams);
    
            addView(progressView);
            addView(textView);
    
            setClipChildren(false);
            setClipToPadding(false);
        }
    
        private void initAttributes(AttributeSet attrs) {
            TypedArray a = getContext().getTheme().obtainStyledAttributes(
                    attrs,
                    R.styleable.LabeledVerticalProgressBar,
                    0, 0);
            try {
                progressDrawable = a.getDrawable(R.styleable.LabeledVerticalProgressBar_progress_drawable);
                min = a.getFloat(R.styleable.LabeledVerticalProgressBar_min, 0);
                max = a.getFloat(R.styleable.LabeledVerticalProgressBar_max, 100);
                progress = a.getFloat(R.styleable.LabeledVerticalProgressBar_progress, 0);
    
                initTextAppearance(a);
    
                int labelPos = a.getInt(R.styleable.LabeledVerticalProgressBar_label_position, 0);
                if (labelPos == 1) {
                    isLabelAbove = false;
                }
                numDecimals = a.getInt(R.styleable.LabeledVerticalProgressBar_num_decimals, 0);
                unit = a.getString(R.styleable.LabeledVerticalProgressBar_unit);
    
                if (min >= max) {
                    throw new IllegalArgumentException("max should be greater than min");
                }
                clampProgress();
            } finally {
                a.recycle();
            }
        }
    
        private void initTextAppearance(TypedArray a) {
            /*TypedValue styleId = new TypedValue();
            boolean resolved = getContext().getTheme().resolveAttribute(R.styleable.LabeledVerticalProgressBar_label_text_appearance,
                    styleId, true);
            if (resolved) {
                labelTextAppearanceId = styleId.data;
            } else {
                labelTextAppearanceId = -1;
            }*/
            ColorStateList color = a.getColorStateList(R.styleable.LabeledVerticalProgressBar_label_text_color);
            int size = a.getDimensionPixelSize(R.styleable.LabeledVerticalProgressBar_label_text_size, -1);
            int style = a.getInt(R.styleable.LabeledVerticalProgressBar_label_text_style, -1);
            Drawable background = a.getDrawable(R.styleable.LabeledVerticalProgressBar_label_background);
    
            if (color != null) {
                textView.setTextColor(color);
            }
            if (size != -1) {
                textView.setTextSize(size);
            }
            if (style != -1) {
                switch (style) {
                    case 0: textView.setTypeface(textView.getTypeface(), Typeface.NORMAL); break;
                    case 1: textView.setTypeface(textView.getTypeface(), Typeface.BOLD); break;
                    case 2: textView.setTypeface(textView.getTypeface(), Typeface.ITALIC); break;
                    case 3: textView.setTypeface(textView.getTypeface(), Typeface.BOLD_ITALIC); break;
                }
            }
            if (background != null) {
                textView.setBackground(background);
            }
        }
    
        private void initProgress() {
            if (progressDrawable == null) {
                progressDrawable = initDefaultProgressDrawable();
            }
            updateProgressView();
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            height = progressView.getMeasuredHeight();
            initLabel();
            if (isLabelAbove) {
                // Add top space for label when progress is at max.
                setPadding(0, textView.getMeasuredHeight(), 0, 0);
            }
            super.onLayout(changed, left, top, right, bottom);
        }
    
        private void initLabel() {
            updateLabel();
            /*if (labelTextAppearanceId != -1) {
                TextViewCompat.setTextAppearance(textView, labelTextAppearanceId);
            }*/
        }
    
        public void setProgress(float progress) {
            this.progress = progress;
            clampProgress();
            progressView.getBackground().setLevel(computeLevel());
            updateLabel();
        }
    
        public void setProgress(final float progress, boolean animate, int duration) {
            if (animate) {
                float end = clampProgress(progress);
                float start = this.progress;
                ValueAnimator animator = ValueAnimator.ofFloat(start, end);
                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        LabeledVerticalProgressBar.this.progress = (float) animation.getAnimatedValue();
                        progressView.getBackground().setLevel(computeLevel());
                        updateLabel();
                    }
                });
                animator.setDuration(duration);
                animator.start();
    
            } else {
                setProgress(progress);
            }
        }
    
        public void setMax(float max) {
            this.max = max;
            if (min >= max) {
                throw new IllegalArgumentException("max should be greater than min");
            }
            setProgress(progress);
        }
    
        public void setMin(float min) {
            this.min = min;
            if (min >= max) {
                throw new IllegalArgumentException("max should be greater than min");
            }
            setProgress(progress);
        }
    
        public void setUnit(String unit) {
            this.unit = unit;
        }
    
        public void setProgressDrawableColor(int color) {
            progressDrawable = new ColorDrawable(color);
            updateProgressView();
        }
    
        public void setProgressDrawable(Drawable progressDrawable) {
            this.progressDrawable = progressDrawable;
            updateProgressView();
        }
    
        private void updateProgressView() {
            ClipDrawable clip = new ClipDrawable(progressDrawable, Gravity.BOTTOM, ClipDrawable.VERTICAL);
            progressView.setBackground(clip);
            clip.setLevel(computeLevel());
        }
    
        private void updateLabel() {
            float translation = computeLabelTranslation();
            if (isLabelAbove) {
                textView.setTranslationY(translation);
            } else {
                if (-translation > textView.getMeasuredHeight()) {
                    textView.setTranslationY(translation + textView.getMeasuredHeight());
                } else {
                    textView.setTranslationY(0);
                }
            }
            String progressStr = String.format("%." + numDecimals + "f", progress) + unit;
            textView.setText(progressStr);
        }
    
        private int computeLevel() {
            float fraction = computeProgressFraction();
            return (int) (fraction * 10000);
        }
    
        private float computeLabelTranslation() {
            return -computeProgressFraction() * height;
        }
    
        private float computeProgressFraction() {
            clampProgress();
            return (progress - min) / (max - min);
        }
    
        private Drawable initDefaultProgressDrawable() {
            int colorAttr = getContext().getResources().getIdentifier("colorPrimary",
                    "attr", getContext().getPackageName());
            TypedValue outValue = new TypedValue();
            getContext().getTheme().resolveAttribute(colorAttr, outValue, true);
            return new ColorDrawable(outValue.data);
        }
    
        private void clampProgress() {
            if (progress > max) {
                progress = max;
            } else if (progress < min) {
                progress = min;
            }
        }
    
        private float clampProgress(float progress) {
            if (progress > max) {
                return max;
            } else if (progress < min) {
                return min;
            } else {
                return progress;
            }
        }
    }
    

    attrs.xml

    <resources>
        <declare-styleable name="LabeledVerticalProgressBar">
            <attr name="progress_drawable" format="reference|color"/>
            <attr name="min" format="float"/>
            <attr name="max" format="float"/>
            <attr name="progress" format="float"/>
            <attr name="label_text_appearance" format="reference"/>
            <attr name="label_text_color" format="color"/>
            <attr name="label_text_size" format="dimension"/>
            <attr name="label_text_style" format="enum">
                <enum name="normal" value="0"/>
                <enum name="bold" value="1"/>
                <enum name="italic" value="2"/>
                <enum name="bold_italic" value="3"/>
            </attr>
            <attr name="label_background" format="reference|color"/>
            <attr name="label_position" format="enum">
                <enum name="above" value="0"/>
                <enum name="below" value="1"/>
            </attr>
    
            <!--Number of displayed decimal places for label-->
            <attr name="num_decimals" format="integer"/>
    
            <attr name="unit" format="string"/>
        </declare-styleable>
    </resources>
    

    Usage:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <marabillas.loremar.bindtextviewandprogressbar.LabeledVerticalProgressBar
            android:id="@+id/progressBar"
            android:layout_width="80dp"
            android:layout_height="480dp"
            android:background="#ddd"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:progress_drawable="#6a0dad"
            app:label_text_color="#fff"
            app:label_text_size="24sp"
            app:label_text_style="bold"
            app:label_background="#4ccc"
            app:label_position="below"
            app:min="20"
            app:max="500"
            app:progress="20"
            app:num_decimals="0"
            app:unit="%"/>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
    @Override
        protected void onStart() {
            super.onStart();
            final LabeledVerticalProgressBar progressBar = findViewById(R.id.progressBar);
            progressBar.setProgress(500, true, 3000);
    
            Handler handler = new Handler(Looper.getMainLooper());
    
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    progressBar.setProgressDrawableColor(Color.GREEN);
                }
            }, 2000);
    
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    GradientDrawable gradient = new GradientDrawable();
                    int color1 = Color.HSVToColor(new float[]{240f, 1f, 1f});
                    int color2 = Color.HSVToColor(new float[]{240f, 1f, 0.5f});
                    int[] colors = { color1, color2, color1};
                    gradient.setColors(colors);
                    gradient.setGradientType(GradientDrawable.LINEAR_GRADIENT);
                    gradient.setOrientation(GradientDrawable.Orientation.LEFT_RIGHT);
                    progressBar.setProgressDrawable(gradient);
    
                    progressBar.setMax(2000);
                    progressBar.setProgress(4000);
                    progressBar.setMin(1000);
                    progressBar.setUnit("");
                    progressBar.setProgress(1500, true, 3000);
                }
            }, 3000);
        }
    

    Results:

    enter image description here

    You can use setProgess() to set progress without animation or pass false to animate parameter.

    As for now, I can't seem to make text appearance for label work.

    I think with some few edits, you can also make this horizontal.