Я хотел бы использовать пользовательские переключатели в приложении Android, которое должно предназначаться для API 22 и более поздних версий. Я хотел бы иметь что-то вроде этого
После завершения источника для переключателя текст и значения будут поступать из базы данных 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>