【Android】自定义View —— 动态修改皮肤

【关键词】

修改皮肤 自定义View

【 问题】

  • 让改变皮肤变得简单;

【效果图】

【Android】自定义View —— 动态修改皮肤_第1张图片

【分析】

「动态加载皮肤分析」

  • 改变背景后将颜色值保存到SharedPreferences中;
  • 当切换或回退到另一个界面,在显示之前,即对应声明周期onStart中对背景进行变化(也可以在设置背景的时候通过广播的方式及时修改另一个界面的背景)

「自定义控件分析」

  • 每一次都只选择一个颜色,根据这个特性,我选择了继承RadioGroup,每一个元素都是一个RadioButton(这样,有多少个背景,就添加多少个对应的颜色元素);
  • 通过Selector来覆盖RadioButton的默认样式,选中的元素,圆圈要大一些(颜色通过java代码动态控制);
  • 改变颜色时能及时获取到值(对外提供一个改变时的接口回调,将改变的值回传给客户端);
  • 可以直接让某个颜色处于选中状态;
  • 设置点击时的效果,增加趣味性;

【难点】

  • 动态修改颜色和选中状态;
  • 当元素比较多时的滑动处理;

【解决方案】

  • 参考代码;

【代码】

「Activity代码」

public class SkinChangeActivity extends Activity {
    private Activity mContext;
    private RatioColor mRatioColor;
    private ViewGroup mLayout;
    private SharedPreferencesUtil mSpu;
    private static final int[] COLORS = new int[]{
            Color.parseColor("#990000"),
            Color.parseColor("#009900"),
            Color.parseColor("#000099"),
            Color.parseColor("#009999"),
            Color.parseColor("#990099"),
            Color.parseColor("#999900"),
            Color.parseColor("#999999"),
            Color.LTGRAY,
            Color.RED,
            Color.CYAN,
            Color.DKGRAY,
            Color.YELLOW,
            Color.GREEN,
            Color.BLACK,
            Color.WHITE,
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = this;
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_skin_change);

        mSpu = SharedPreferencesUtil.getInstance(mContext);

        initView();
    }


    private void initView() {
        mLayout = (ViewGroup) findViewById(R.id.llyt_main);
        if (mLayout == null) {
            throw new NullPointerException("未找到视图");
        }

        mRatioColor = new RatioColor(mContext);
        mRatioColor.addItems(COLORS);
        mRatioColor.setOnCheckedColorListener(new RatioColor.onCheckedColorListener() {
            @Override
            public void doColor(int color) {
                setCurrentBgColor(color);
            }
        });

        mLayout.addView(mRatioColor);

        // 设置当前背景色
        setCurrentBgColor(mSpu.getSkin());
    }


    public void setCurrentBgColor(int color) {
        mLayout.setBackgroundColor(color);
        mRatioColor.setCheckedColor(color);
        if (mSpu.getSkin() != color) {
            mSpu.saveSkin(color);
        }
    }
}

「布局代码」activity_skin_change.xml(只是一个LinearLayout布局)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/llyt_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp"/>

「通用的自定义View源码」

public class RatioColor extends RadioGroup {
    public interface onCheckedColorListener {
        void doColor(int color);
    }

    private onCheckedColorListener mColorListener;
    private final Context CONTEXT = getContext();
    private final int W = Uscreen.dp2Px(CONTEXT, 48);

    public RatioColor(Context context) {
        this(context, null);
    }

    public RatioColor(Context context, AttributeSet attrs) {
        super(context, attrs);
        setUp();
    }


    private void setUp() {
        // 设置默认宽高
        setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, W));
        // 设置默认样式(背景色,方向,重心)
        setProperties(Color.parseColor("#eeeeee"), RadioGroup.HORIZONTAL, Gravity.START);
    }


    public void setProperties(int bgColor, int orientation, int gravity) {
        setBackgroundColor(bgColor);
        setOrientation(orientation);
        setGravity(gravity);

        ViewGroup.LayoutParams params = getLayoutParams();
        // 在父布局里居左显示
        if (params instanceof LinearLayout.LayoutParams) {
            ((LinearLayout.LayoutParams) params).gravity = Gravity.START;
        }
        // 动态更改方向
        if (orientation == VERTICAL) {
            params.width = W;
            params.height = ViewGroup.LayoutParams.MATCH_PARENT;
        } else {
            params.width = ViewGroup.LayoutParams.MATCH_PARENT;
            params.height = W;
        }
    }

    /** * 设置颜色变化的监听 */

    public void setOnCheckedColorListener(onCheckedColorListener listener) {
        mColorListener = listener;
    }


    /** * 根据一组颜色值添加一组元素 */

    public void addItems(int[] colorArray) {
        for (int color : colorArray) {
            addItem(color);
        }
    }

    /** * 重新设置一组元素 */

    public void resetItems(int[] colorArray) {
        removeAllViews();
        if (colorArray != null)
            addItems(colorArray);
    }

    /** * 根据颜色值添加一个元素 * * @param color */
    public void addItem(final int color) {

        RadioButton rbtn = new RadioButton(CONTEXT);
        rbtn.setFocusable(false);
        rbtn.setFocusableInTouchMode(false);
        rbtn.setLayoutParams(new RadioGroup.LayoutParams(W, W));
        rbtn.setButtonDrawable(new ColorDrawable(Color.TRANSPARENT)); // 取消默认的样式
        rbtn.setBackgroundResource(R.drawable.selector_skin_color); // 设置自己的样式(shape)

        // 动态设置颜色值(基于selector)
        StateListDrawable gradientDrawable = (StateListDrawable) rbtn.getBackground();
        DrawableContainer.DrawableContainerState drawableContainerState = (DrawableContainer.DrawableContainerState) gradientDrawable.getConstantState();
        Drawable[] children = drawableContainerState.getChildren();
        for (Drawable child : children) {
            if (child instanceof LayerDrawable) {
                LayerDrawable layerDrawable = (LayerDrawable) child;
                Drawable drawable = layerDrawable.getDrawable(0);
                if (drawable instanceof GradientDrawable) {
                    GradientDrawable selectedDrawable = (GradientDrawable) drawable;
                    selectedDrawable.mutate(); // 此句不可少
                    selectedDrawable.setColor(color);
                }
            }
        }

        // 将颜色信息保存到Tag中
        rbtn.setTag(color);
        clickEffectByScaleAnim(rbtn, mOnCheckedChangeListener);
        addView(rbtn);
    }

    /** * 设置某个颜色为选中状态 * * @param checkedColor */
    public void setCheckedColor(int checkedColor) {

        for (int i = 0; i < getChildCount(); i++) {
            CompoundButton cbtn = (CompoundButton) getChildAt(i);
            int color = (int) cbtn.getTag();
            if (checkedColor == color) {
                cbtn.setChecked(true);
            }
        }
    }

    /** * 获取当前选中的颜色 * * @return */
    public int getCheckedColor() {
        for (int i = 0; i < getChildCount(); i++) {
            CompoundButton cbtn = (CompoundButton) getChildAt(i);
            if (cbtn.isChecked()) {
                return (int) cbtn.getTag();
            }
        }
        return 0;
    }

    // 共用一个listener,避免多次创建消耗过多内存
    private final CompoundButton.OnCheckedChangeListener mOnCheckedChangeListener = new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            if (isChecked) {
                // 从Tag中取出color信息
                int color = (int) buttonView.getTag();
                if (mColorListener != null) {
                    mColorListener.doColor(color);
                }
            }
        }
    };


    // -------------------- 滑动处理
    private int mLastX;
    private int mLastY;
    private Scroller mScroller = new Scroller(CONTEXT);

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

    private void smoothScrollTo(int dextX) {
        smoothScrollBy(dextX - getScrollX());
    }

    private void smoothScrollBy(int deltaX) {
        mScroller.startScroll(getScrollX(), 0, deltaX, 0, 480);
        invalidate();
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = (int) event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);
                break;
            case MotionEvent.ACTION_UP:
                // 松开后判断是否需要回弹
                int scrollX = getScrollX();
                int maxX = getChildCount() * W - getWidth();
                if (scrollX < 0) {
                    smoothScrollTo(0);
                } else if (scrollX > maxX) {
                    smoothScrollTo(maxX);
                }
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }


    // 滑动冲突处理
    private float mLastXIntercepted;
    private float mLastYIntercepted;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                // 移动的水平距离大于垂直距离时,进行拦截
                intercepted = Math.abs(x - mLastXIntercepted) >= Math.abs(y - mLastYIntercepted) + 4;
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
            default:
                break;
        }

        mLastX = x; // 此句不可少
        mLastY = y;
        mLastXIntercepted = x;
        mLastYIntercepted = y;
        return intercepted;
    }

    // ~~~~~~~~~~~~~~~~~~~~

    // -------------------- 工具方法
    public static void clickEffectByScaleAnim(final RadioButton radioButton,
                                              RadioButton.OnCheckedChangeListener listener) {
        // 设置监听
        radioButton.setOnCheckedChangeListener(listener);

        // 设置触摸效果
        radioButton.setOnTouchListener(new OnTouchListener() {

            @SuppressLint("ClickableViewAccessibility")
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (radioButton.isChecked()) { // 如果已经是选中状态,则不做任何效果处理;
                    return false;
                }
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        doDown(v);
                        break;
                    case MotionEvent.ACTION_MOVE:
                        break;
                    case MotionEvent.ACTION_CANCEL:
                        doCancel(v);
                        break;
                    case MotionEvent.ACTION_UP:
                        doUp(v);
                        break;
                }
                return false;
            }

            // 按下时开始动画
            private void doDown(View view) {
                view.startAnimation(newAnimation(1f, 2f, 1000, true));
            }

            // 恢复视图
            private void doUp(View view) {
                view.clearAnimation();
                view.startAnimation(newAnimation(0.4f, 1f, 400, false));
            }

            // 取消动画
            private void doCancel(View view) {
                view.clearAnimation();
            }
        });
    }

    public static Animation newAnimation(float from, float to, int duration, boolean repeat) {
        Animation anim = new ScaleAnimation(from, to, from, to, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        anim.setDuration(duration);
        anim.setFillAfter(false);
        if (repeat) {
            anim.setRepeatCount(Animation.INFINITE);
            anim.setRepeatMode(Animation.REVERSE);
            anim.setInterpolator(new BounceInterpolator());
        }
        return anim;
    }
    // ~~~~~~~~~~~~~~~~~~~~
}

「Selector代码」

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true">
        <layer-list>
            <item android:bottom="4dp" android:left="4dp" android:right="4dp" android:top="4dp">
                <shape android:shape="oval">
                    <solid android:color="@android:color/transparent" />
                </shape>
            </item>
        </layer-list>
    </item>

    <item android:state_checked="false">
        <layer-list>
            <item android:bottom="12dp" android:left="12dp" android:right="12dp" android:top="12dp">
                <shape android:shape="oval">
                    <solid android:color="@android:color/transparent" />
                </shape>
            </item>
        </layer-list>
    </item>
</selector>

【扩展】

  • 目前对控件只做了横向处理,可以尝试下对控件进行纵向布局并处理滑动和回弹效果处理;

【参考资料】

  • listview所带来的滑动冲突
  • Android-onInterceptTouchEvent()和onTouchEvent()总结

    以此类推,我们可以得到各种具体的情况,整个layout的view类层次中都有机会截获,而且能看出来外围的容器view具有优先截获权。

  • 《Android开发艺术探索》第三章——View的滑动冲突

你可能感兴趣的:(android,自定义view,RatioColor)