Программируемое добавление переключателя не работает в Android API 22 - PullRequest
0 голосов
/ 01 ноября 2018

Я хотел бы использовать пользовательские переключатели в приложении Android, которое должно предназначаться для API 22 и более поздних версий. Я хотел бы иметь что-то вроде этого

look of requried radio buttons

После завершения источника для переключателя текст и значения будут поступать из базы данных SQLite, и поэтому мне потребуется возможность добавлять переключатели программно, а не определять их в файле макета XML.

Я попытался сделать это, используя код, описанный в этом сообщении в блоге https://crosp.net/blog/software-development/mobile/android/creating-custom-radio-groups-radio-buttons-android/, а код, который он описывает, можно найти здесь https://github.com/CROSP/custom-radio-group-radio-button-android.

Я создал пример, который работает, когда я пробую его на моем эмуляторе, который является API 25. Однако проблема возникает, когда я запускаю его на устройстве / эмуляторе, который является API 22. Используя Android Studio, я запустил, используя Эмулятор Nexus 4 API 22, но, к сожалению, код не работает. Когда я говорю, что это не работает, все, что вы видите, это пустой экран, а не пользовательские переключатели, которые я вижу, когда я запускаю на API 25. Я посмотрел на logcat, и там нет никаких сообщений об ошибках .

Итак, в целом, у меня есть некоторый код, который работает для API 25, но не для API 22. И я хотел бы знать, как исправить код, чтобы он работал в API 22.

Я создал самую базовую версию кода, которая может повторять проблему. Этот код ниже

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);
        }
    }

Следующие java-файлы очень похожи на те, которые можно найти в 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();
        }
    }
}

папка макета 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>

выдвижная папка

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>
Папка

значений 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>
...