0

I would like to use custom radio buttons in an Android app that must target API 22 and later. I'd like to have something like this

look of requried radio buttons

When complete the source for the radio button text and values will come from an SQLite database and therefore I will need to be able to add the radio buttons programmatically rather than defining them in the XML layout file.

I've attempted this by using the code described in this blog post https://crosp.net/blog/software-development/mobile/android/creating-custom-radio-groups-radio-buttons-android/ and the code it describes can be found here https://github.com/CROSP/custom-radio-group-radio-button-android.

I've created an example that works when I try it on my emulator that is API 25. However, the problem comes when I run it on a device/emulator that is API 22. Using Android Studio I have run using the Nexus 4 API 22 emulator but unfortunately, the code does not work. When I say it doesn't work all you see is a blank screen rather than the custom radio buttons that I see when I run on API 25. I've looked at the logcat and there does not appear to be any errors that are reported.

So, in summary, I have some code that works for API 25 but not for API 22. And I'd like to know how to fix the code so that it will work in API 22.

I've created the most basic version of the code that I can that replicates the problem. This code is below

MainActivity.java

package net.myexample.radioapp;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.LinearLayout;
import net.myexample.radioapp.CustomRadioButtons.PresetRadioGroup;
import net.myexample.radioapp.CustomRadioButtons.PresetValueButton;

 public class MainActivity extends AppCompatActivity
    {
        private PresetRadioGroup mPledgeDescriptionsContainer;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mPledgeDescriptionsContainer = 
            findViewById(R.id.pledge_descriptions_container);
            int counter =0;
            do {
                String description = "test " + counter;
                double amount = counter;
                PresetValueButton rbtn = new PresetValueButton(this);
                rbtn.setId(counter);
                rbtn.setLayoutParams(new 
    LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1.0f));
                rbtn.setUnit(description);
                rbtn.setValue("£" + amount);
                mPledgeDescriptionsContainer.addView(rbtn);
                counter++;
            } while (counter < 5);
        }
    }

The following java files are very similar to those found in https://github.com/CROSP/custom-radio-group-radio-button-android

PresetRadioGroup.java

package net.myexample.radioapp.CustomRadioButtons;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.support.annotation.IdRes;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import net.myexample.radioapp.R;
import java.util.HashMap;

public class PresetRadioGroup extends LinearLayout {


    private int mCheckedId = View.NO_ID;
    private boolean mProtectFromCheckedChange = false;
    private OnCheckedChangeListener mOnCheckedChangeListener;
    private HashMap<Integer, View> mChildViewsMap = new HashMap<>();
    private PassThroughHierarchyChangeListener mPassThroughListener;
    private RadioCheckable.OnCheckedChangeListener mChildOnCheckedChangeListener;

    public PresetRadioGroup(Context context) {
        super(context);
        setupView();
    }

    public PresetRadioGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        parseAttributes(attrs);
        setupView();
    }

    @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
    public PresetRadioGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        parseAttributes(attrs);
        setupView();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public PresetRadioGroup(
            Context context,
            AttributeSet attrs,
            int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        parseAttributes(attrs);
        setupView();
    }

    private void parseAttributes(AttributeSet attrs) {
        TypedArray a = getContext().obtainStyledAttributes(attrs,
                R.styleable.PresetRadioGroup, 0, 0);
        try {
            mCheckedId =
                    a.getResourceId(R.styleable.PresetRadioGroup_presetRadioCheckedId,
                    View.NO_ID);
        } finally {
            a.recycle();
        }
    }

    private void setupView() {
        mChildOnCheckedChangeListener = new CheckedStateTracker();
        mPassThroughListener = new PassThroughHierarchyChangeListener();
        super.setOnHierarchyChangeListener(mPassThroughListener);
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if (child instanceof RadioCheckable) {
            final RadioCheckable button = (RadioCheckable) child;
            if (button.isChecked()) {
                mProtectFromCheckedChange = true;
                if (mCheckedId != View.NO_ID) {
                    setCheckedStateForView(mCheckedId, false);
                }
                mProtectFromCheckedChange = false;
                setCheckedId(child.getId(), true);
            }
        }
        super.addView(child, index, params);
    }

    @Override
    public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
        // the user listener is delegated to our pass-through listener
        mPassThroughListener.mOnHierarchyChangeListener = listener;
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (mCheckedId != View.NO_ID) {
            mProtectFromCheckedChange = true;
            setCheckedStateForView(mCheckedId, true);
            mProtectFromCheckedChange = false;
            setCheckedId(mCheckedId, true);
        }
    }

    private void setCheckedId(@IdRes int id, boolean isChecked) {
        mCheckedId = id;
        if (mOnCheckedChangeListener != null) {
            mOnCheckedChangeListener.onCheckedChanged(this,
                    mChildViewsMap.get(id), isChecked, mCheckedId);
        }
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    public void clearCheck() {
        check(View.NO_ID);
    }

    public void check(@IdRes int id) {
        if (id != View.NO_ID && (id == mCheckedId)) {
            return;
        }

        if (mCheckedId != View.NO_ID) {
            setCheckedStateForView(mCheckedId, false);
        }

        if (id != View.NO_ID) {
            setCheckedStateForView(id, true);
        }
        setCheckedId(id, true);
    }

    private void setCheckedStateForView(int viewId, boolean checked) {
        View checkedView;
        checkedView = mChildViewsMap.get(viewId);
        if (checkedView == null) {
            checkedView = findViewById(viewId);
            if (checkedView != null) {
                mChildViewsMap.put(viewId, checkedView);
            }
        }
        if (checkedView != null && checkedView instanceof RadioCheckable) {
            ((RadioCheckable) checkedView).setChecked(checked);
        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    public void setOnCheckedChangeListener(
            OnCheckedChangeListener onCheckedChangeListener) {
        mOnCheckedChangeListener = onCheckedChangeListener;
    }

    public OnCheckedChangeListener getOnCheckedChangeListener() {
        return mOnCheckedChangeListener;
    }

    public static interface OnCheckedChangeListener {
        void onCheckedChanged(
                View radioGroup, View radioButton, boolean isChecked, int checkedId);
    }

    public static class LayoutParams extends LinearLayout.LayoutParams {
        /**
         * {@inheritDoc}
         */
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        /**
         * {@inheritDoc}
         */
        public LayoutParams(int w, int h) {
            super(w, h);
        }

        /**
         * {@inheritDoc}
         */
        public LayoutParams(int w, int h, float initWeight) {
            super(w, h, initWeight);
        }

        /**
         * {@inheritDoc}
         */
        public LayoutParams(ViewGroup.LayoutParams p) {
            super(p);
        }

        /**
         * {@inheritDoc}
         */
        public LayoutParams(MarginLayoutParams source) {
            super(source);
        }

        /**
         * <p>Fixes the child's width to
         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's
         * height to  {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
         * when not specified in the XML file.</p>
         *
         * @param a          the styled attributes set
         * @param widthAttr  the width attribute to fetch
         * @param heightAttr the height attribute to fetch
         */
        @Override
        protected void setBaseAttributes(TypedArray a,
                                         int widthAttr, int heightAttr) {

            if (a.hasValue(widthAttr)) {
                width = a.getLayoutDimension(widthAttr, "layout_width");
            } else {
                width = WRAP_CONTENT;
            }

            if (a.hasValue(heightAttr)) {
                height = a.getLayoutDimension(heightAttr, "layout_height");
            } else {
                height = WRAP_CONTENT;
            }
        }
    }

    private class CheckedStateTracker implements RadioCheckable.OnCheckedChangeListener {
        @Override
        public void onCheckedChanged(View buttonView, boolean isChecked) {
            // prevents from infinite recursion
            if (mProtectFromCheckedChange) {
                return;
            }

            mProtectFromCheckedChange = true;
            if (mCheckedId != View.NO_ID) {
                setCheckedStateForView(mCheckedId, false);
            }
            mProtectFromCheckedChange = false;

            int id = buttonView.getId();
            setCheckedId(id, true);
        }
    }

    private class PassThroughHierarchyChangeListener implements
            ViewGroup.OnHierarchyChangeListener {
        private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;

        /**
         * {@inheritDoc}
         */
        public void onChildViewAdded(View parent, View child) {
            if (parent == PresetRadioGroup.this && child instanceof RadioCheckable) {
                int id = child.getId();
                // generates an id if it's missing
                if (id == View.NO_ID) {
                    id = ViewUtils.generateViewId();
                    child.setId(id);
                }
                ((RadioCheckable) child).addOnCheckChangeListener(
                        mChildOnCheckedChangeListener);
                mChildViewsMap.put(id, child);
            }

            if (mOnHierarchyChangeListener != null) {
                mOnHierarchyChangeListener.onChildViewAdded(parent, child);
            }
        }

        /**
         * {@inheritDoc}
         */
        public void onChildViewRemoved(View parent, View child) {
            if (parent == PresetRadioGroup.this && child instanceof RadioCheckable) {
                ((RadioCheckable) child)
                        .removeOnCheckChangeListener(mChildOnCheckedChangeListener);
            }
            mChildViewsMap.remove(child.getId());
            if (mOnHierarchyChangeListener != null) {
                mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
            }
        }
    }
}

PresetValueButton.java

  package net.myexample.radioapp.CustomRadioButtons;
    import android.content.Context;
    import android.content.res.Resources;
    import android.content.res.TypedArray;
    import android.graphics.Color;
    import android.graphics.drawable.Drawable;
    import android.os.Build;
    import android.support.annotation.Nullable;
    import android.support.annotation.RequiresApi;
    import android.util.AttributeSet;
    import android.view.LayoutInflater;
    import android.view.MotionEvent;
    import android.view.View;
    import android.widget.RelativeLayout;
    import android.widget.TextView;
    import net.myexample.radioapp.R;
    import java.util.ArrayList;


    public class PresetValueButton extends RelativeLayout implements RadioCheckable {
        private TextView mValueTextView, mUnitTextView;
        public static final int DEFAULT_TEXT_COLOR = Color.WHITE;
        private String mValue;
        private String mUnit;
        private int mValueTextColor;
        private int mUnitTextColor;
        private int mPressedTextColor;
        private Drawable mInitialBackgroundDrawable;
        private OnClickListener mOnClickListener;
        private OnTouchListener mOnTouchListener;
        private boolean mChecked;
        private ArrayList<OnCheckedChangeListener> mOnCheckedChangeListeners 
                = new ArrayList<>();

        public PresetValueButton(Context context) {
            super(context, null, R.style.PresetLayoutButton);
            mValueTextColor = Color.WHITE;
            mUnitTextColor = Color.WHITE;
            mPressedTextColor =Color.WHITE;
            setupView();
        }

        public PresetValueButton(Context context, AttributeSet attrs) {
            super(context, attrs);
            parseAttributes(attrs);
            setupView();
        }

        @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
        public PresetValueButton(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            parseAttributes(attrs);
            setupView();
        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        public PresetValueButton(Context context, AttributeSet attrs,
                                 int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            parseAttributes(attrs);
            setupView();
        }

        private void parseAttributes(AttributeSet attrs) {
            TypedArray a = getContext().obtainStyledAttributes(attrs,
                    R.styleable.PresetValueButton, 0, 0);
            Resources resources = getContext().getResources();
            try {
                mValue = a.getString(R.styleable.PresetValueButton_presetButtonValueText);
                mUnit = a.getString(R.styleable.PresetValueButton_presetButtonUnitText);
                mValueTextColor = a.getColor(
                        R.styleable.PresetValueButton_presetButtonValueTextColor,
                        resources.getColor(R.color.fontWhite));
                mPressedTextColor = a.getColor(
                        R.styleable.PresetValueButton_presetButtonPressedTextColor
                        , Color.WHITE);
                mUnitTextColor = a.getColor(
                        R.styleable.PresetValueButton_presetButtonUnitTextColor
                        , resources.getColor(R.color.fontWhite));
            } finally {
                a.recycle();
            }
        }

        private void setupView() {
            inflateView();
            bindView();
            setCustomTouchListener();
        }

        protected void inflateView() {
            LayoutInflater inflater = LayoutInflater.from(getContext());
            inflater.inflate(R.layout.custom_preset_button, this, true);
            mValueTextView = (TextView) findViewById(R.id.text_view_value);
            mUnitTextView = (TextView) findViewById(R.id.text_view_unit);
            mInitialBackgroundDrawable = getBackground();
        }

        protected void bindView() {
            if (mUnitTextColor != DEFAULT_TEXT_COLOR) {
                mUnitTextView.setTextColor(mUnitTextColor);
            }
            if (mValueTextColor != DEFAULT_TEXT_COLOR) {
                mValueTextView.setTextColor(mValueTextColor);
            }
            mUnitTextView.setText(mUnit);
            mValueTextView.setText(mValue);
        }

        @Override
        public void setOnClickListener(@Nullable OnClickListener l) {
            mOnClickListener = l;
        }

        protected void setCustomTouchListener() {
            super.setOnTouchListener(new TouchListener());
        }

        @Override
        public void setOnTouchListener(OnTouchListener onTouchListener) {
            mOnTouchListener = onTouchListener;
        }

        public OnTouchListener getOnTouchListener() {
            return mOnTouchListener;
        }

        private void onTouchDown(MotionEvent motionEvent) {
            setChecked(true);
        }

        private void onTouchUp(MotionEvent motionEvent) {
            // Handle user defined click listeners
            if (mOnClickListener != null) {
                mOnClickListener.onClick(this);
            }
        }

        public void setCheckedState() {
            setBackgroundResource(R.drawable.background_shape_preset_button__pressed);
            mValueTextView.setTextColor(mPressedTextColor);
            mUnitTextView.setTextColor(mPressedTextColor);
        }

        public void setNormalState() {
            setBackgroundDrawable(mInitialBackgroundDrawable);
            mValueTextView.setTextColor(mValueTextColor);
            mUnitTextView.setTextColor(mUnitTextColor);
        }

        public String getValue() {
            return mValue;
        }

        public void setValue(String value) {
            mValue = value;
            mValueTextView.setText(value);
        }

        public String getUnit() {
            return mUnit;
        }

        public void setUnit(String unit) {
            mUnit = unit;
            mUnitTextView.setText(unit);
        }

        @Override
        public void setChecked(boolean checked) {
            if (mChecked != checked) {
                mChecked = checked;
                if (!mOnCheckedChangeListeners.isEmpty()) {
                    for (int i = 0; i < mOnCheckedChangeListeners.size(); i++) {
                        mOnCheckedChangeListeners.get(i).onCheckedChanged(this, mChecked);
                    }
                }
                if (mChecked) {
                    setCheckedState();
                } else {
                    setNormalState();
                }
            }
        }

        @Override
        public boolean isChecked() {
            return mChecked;
        }

        @Override
        public void toggle() {
            setChecked(!mChecked);
        }

        @Override
        public void addOnCheckChangeListener(OnCheckedChangeListener onCheckedChangeListener) {
            mOnCheckedChangeListeners.add(onCheckedChangeListener);
        }

        @Override
        public void removeOnCheckChangeListener(
                OnCheckedChangeListener onCheckedChangeListener) {
            mOnCheckedChangeListeners.remove(onCheckedChangeListener);
        }

        private final class TouchListener implements OnTouchListener {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        onTouchDown(event);
                        break;
                    case MotionEvent.ACTION_UP:
                        onTouchUp(event);
                        break;
                }
                if (mOnTouchListener != null) {
                    mOnTouchListener.onTouch(v, event);
                }
                return true;
            }
        }
    }

RadioCheckable.java

package net.myexample.radioapp.CustomRadioButtons;
import android.view.View;
import android.widget.Checkable;

public interface RadioCheckable extends Checkable {
    void addOnCheckChangeListener(OnCheckedChangeListener onCheckedChangeListener);
    void removeOnCheckChangeListener(OnCheckedChangeListener onCheckedChangeListener);

    public static interface OnCheckedChangeListener {
        void onCheckedChanged(View buttonView, boolean isChecked);
    }
}

ViewUtils.java

package net.myexample.radioapp.CustomRadioButtons;
import android.annotation.SuppressLint;
import android.os.Build;
import android.view.View;
import java.util.concurrent.atomic.AtomicInteger;

public class ViewUtils {
    private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);

    @SuppressLint("NewApi")
    public static int generateViewId() {

        if (Build.VERSION.SDK_INT < 17) {
            for (; ; ) {
                final int result = sNextGeneratedId.get();
                // aapt-generated IDs have the high byte nonzero;
                // clamp to the range under that.
                int newValue = result + 1;
                if (newValue > 0x00FFFFFF)
                    newValue = 1; // Roll over to 1, not 0.
                if (sNextGeneratedId.compareAndSet(result, newValue)) {
                    return result;
                }
            }
        } else {
            return View.generateViewId();
        }
    }
}

layout folder activity_main.xml

 <LinearLayout 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="vertical"
    tools:context=".MainActivity">
        <net.myexample.radioapp.CustomRadioButtons.PresetRadioGroup
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/pledge_descriptions_container"
            android:layout_alignParentRight="true"
            android:orientation="vertical">
        </net.myexample.radioapp.CustomRadioButtons.PresetRadioGroup>
    </LinearLayout>

custom_preset_button.xml

<merge xmlns:android="http://schemas.android.com/apk/res/android">
  <TextView
      android:id="@+id/text_view_value"
      style="@style/PresetLayoutButton_ValueText"
      android:layout_width="100dp"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      />
  <TextView
      android:id="@+id/text_view_unit"
      style="@style/PresetLayoutButton_UnitText"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_toRightOf="@+id/text_view_value"
      />
</merge>

drawable folder

background_selector_preset_button.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/background_shape_preset_button__pressed" android:state_focused="true"/>
    <item android:drawable="@drawable/background_shape_preset_button__pressed" android:state_pressed="true"/>
    <item android:drawable="@drawable/background_shape_preset_time_button"/>
</selector>

background_shape_preset_button_pressed.xml

 <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
           android:shape="rectangle">
        <solid android:color="#fe9900"/>
    </shape>

background_shape_preset_time_button.xml

   <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
           android:shape="rectangle">
        <solid android:color="@color/fontWhite"/>
        <stroke
            android:width="0.2dp"
            android:color="#2f3f3f"/>
    </shape>

values folder attrs_preset_radio_group.xml

 <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="PresetRadioGroup">
            <attr name="presetRadioCheckedId" format="reference"/>
        </declare-styleable>
    </resources>

attrs_preset_value_button.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <declare-styleable name="PresetValueButton">
    <attr name="presetButtonValueText" format="integer"/>
    <attr name="presetButtonUnitText" format="string"/>
    <attr name="presetButtonValueTextColor" format="color"/>
    <attr name="presetButtonPressedTextColor" format="color"/>
    <attr name="presetButtonUnitTextColor" format="color"/>
  </declare-styleable>
</resources>

colors.xml

 <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <color name="fontWhite">#FFF</color>
        <color name="main_background">#000</color>
    </resources>

strings.xml

 <resources>
        <string name="app_name">my app</string>
    </resources>

styles.xml

 <resources>
        <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
            <item name="android:windowBackground">@color/main_background</item>
        </style>
        <style name="PresetLayoutButton">
            <item name="android:background">@drawable/background_selector_preset_button</item>
            <item name="android:clickable">true</item>
        </style>
        <style name="PresetLayoutButton_ValueText">
            <item name="android:textColor">@color/fontWhite</item>
        </style>
        <style name="PresetLayoutButton_UnitText">
            <item name="android:textColor">@color/fontWhite</item>
        </style>
    </resources>
Saeed Hassanvand
  • 931
  • 1
  • 14
  • 31
Dave Barnett
  • 2,045
  • 2
  • 16
  • 32
  • can you post some logs or stacktrace – Yash Fatnani Nov 01 '18 at 13:50
  • I can't find much. It doesn't actually error at all so there is no stacktrace to look at. I've just run the code on an Amazon Kindle API 22 and have seen a warning "Attempt to retrieve bag 0x0103003e which is invalid or in a cycle" but it could easily be unrelated. – Dave Barnett Nov 01 '18 at 14:28

0 Answers0