目标效果:
该代码参考鸿洋大神的代码,并进行后期的整理修改。默认为随机填色,如果选择了颜色可以进行指定颜色填充,但填色边的效果暂时还不知道是什么原因。
1.values文件夹中新建attrs.xml页面,自定义属性。
attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ColourImageView">
<attr name="border_color" format="color|reference"></attr>
</declare-styleable>
</resources>
2.新建MyRadioGroup.java页面,重写RadioGroup控件,解决多行多列的单选按钮点击问题。
MyRadioGroup.java页面:
package com.example.weixu.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import java.util.ArrayList;
import java.util.HashMap;
/**
* Created by Dustray on 2017/4/8 0008.
*/
public class MyRadioGroup extends LinearLayout {
private static final String TAG = MyRadioGroup.class.getSimpleName();
// holds the checked id; the selection is empty by default
private int mCheckedId = -1;
// tracks children radio buttons checked state
private CompoundButton.OnCheckedChangeListener mChildOnCheckedChangeListener;
// when true, mOnCheckedChangeListener discards events
private boolean mProtectFromCheckedChange = false;
private OnCheckedChangeListener mOnCheckedChangeListener;
private PassThroughHierarchyChangeListener mPassThroughListener;
private HashMap<Integer, RadioButton> mRadioButtons = new HashMap<>();
/**
* {@inheritDoc}
*/
public MyRadioGroup(Context context) {
super(context);
setOrientation(VERTICAL);
init();
}
/**
* {@inheritDoc}
*/
public MyRadioGroup(Context context, AttributeSet attrs) {
super(context, attrs);
int orientation = this.getOrientation();
setOrientation(orientation);
init();
}
private void init() {
mChildOnCheckedChangeListener = new CheckedStateTracker();
mPassThroughListener = new PassThroughHierarchyChangeListener();
super.setOnHierarchyChangeListener(mPassThroughListener);
}
/**
* {@inheritDoc}
*/
@Override
public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
// the user listener is delegated to our pass-through listener
mPassThroughListener.mOnHierarchyChangeListener = listener;
}
/**
* {@inheritDoc}
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// checks the appropriate radio button as requested in the XML file
if (mCheckedId != -1) {
mProtectFromCheckedChange = true;
setCheckedStateForView(mCheckedId, true);
mProtectFromCheckedChange = false;
setCheckedId(mCheckedId);
}
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (child instanceof RadioButton) {
final RadioButton button = (RadioButton) child;
if (button.isChecked()) {
mProtectFromCheckedChange = true;
if (mCheckedId != -1) {
setCheckedStateForView(mCheckedId, false);
}
mProtectFromCheckedChange = false;
setCheckedId(button.getId());
}
}
super.addView(child, index, params);
}
/**
* <p>Sets the selection to the radio button whose identifier is passed in
* parameter. Using -1 as the selection identifier clears the selection;
* such an operation is equivalent to invoking {@link #clearCheck()}.</p>
*
* @param id the unique id of the radio button to select in this group
*
* @see #getCheckedRadioButtonId()
* @see #clearCheck()
*/
public void check(int id) {
// don't even bother
if (id != -1 && (id == mCheckedId)) {
return;
}
if (mCheckedId != -1) {
setCheckedStateForView(mCheckedId, false);
}
if (id != -1) {
setCheckedStateForView(id, true);
}
setCheckedId(id);
}
private void setCheckedId(int id) {
mCheckedId = id;
if (mOnCheckedChangeListener != null) {
mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
}
}
private void setCheckedStateForView(int viewId, boolean checked) {
View checkedView = findViewById(viewId);
if (checkedView != null && checkedView instanceof RadioButton) {
((RadioButton) checkedView).setChecked(checked);
}
}
/**
* <p>Returns the identifier of the selected radio button in this group.
* Upon empty selection, the returned value is -1.</p>
*
* @return the unique id of the selected radio button in this group
*
* @see #check(int)
* @see #clearCheck()
*
* @attr ref android.R.styleable#RadioGroup_checkedButton
*/
public int getCheckedRadioButtonId() {
return mCheckedId;
}
/**
* <p>Clears the selection. When the selection is cleared, no radio button
* in this group is selected and {@link #getCheckedRadioButtonId()} returns
* null.</p>
*
* @see #check(int)
* @see #getCheckedRadioButtonId()
*/
public void clearCheck() {
check(-1);
}
/**
* <p>Register a callback to be invoked when the checked radio button
* changes in this group.</p>
*
* @param listener the callback to call on checked state change
*/
public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
mOnCheckedChangeListener = listener;
}
/**
* {@inheritDoc}
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
/**
* {@inheritDoc}
*/
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
event.setClassName(MyRadioGroup.class.getName());
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(MyRadioGroup.class.getName());
}
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 ViewGroup.LayoutParams#WRAP_CONTENT} and the child's
* height to {@link 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;
}
}
}
/**
* <p>Interface definition for a callback to be invoked when the checked
* radio button changed in this group.</p>
*/
public interface OnCheckedChangeListener {
/**
* <p>Called when the checked radio button has changed. When the
* selection is cleared, checkedId is -1.</p>
*
* @param group the group in which the checked radio button has changed
* @param checkedId the unique identifier of the newly checked radio button
*/
public void onCheckedChanged(MyRadioGroup group, int checkedId);
}
private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// prevents from infinite recursion
if (mProtectFromCheckedChange) {
return;
}
mProtectFromCheckedChange = true;
if (mCheckedId != -1) {
setCheckedStateForView(mCheckedId, false);
}
mProtectFromCheckedChange = false;
int id = buttonView.getId();
setCheckedId(id);
}
}
public RadioButton getRadioButtonByCheckedId(int checkedId){
return mRadioButtons.get(checkedId);
}
/**
* <p>A pass-through listener acts upon the events and dispatches them
* to another listener. This allows the table layout to set its own internal
* hierarchy change listener without preventing the user to setup his.</p>
*/
private class PassThroughHierarchyChangeListener implements
OnHierarchyChangeListener {
private OnHierarchyChangeListener mOnHierarchyChangeListener;
private int checkedCount = 0;
/**
* 找到布局下所有RadioButton
* @param vg
* @return
*/
public ArrayList<RadioButton> findRadioButtons(ViewGroup vg) {
ArrayList<RadioButton> set = new ArrayList<>();
int count = vg.getChildCount();
for (int i = 0 ; i < count ; i ++) {
View view = vg.getChildAt(i);
if (view instanceof RadioButton) {
set.add(((RadioButton) view));
} else if (view instanceof ViewGroup){
set.addAll(findRadioButtons((ViewGroup) view));
}
}
return set;
}
/**
* 为RadioButton设置id和监听器
* @param rb
*/
public void setIdAndOnCheckedChangeListener(RadioButton rb){
int id = rb.getId();
if (id == View.NO_ID) {
id = View.generateViewId();
rb.setId(id);
}
rb.setOnCheckedChangeListener( mChildOnCheckedChangeListener);
if (rb.isChecked()) {
if (checkedCount ++ > 1) {
Log.e(TAG, "You can only select one RadioButton");
}
mCheckedId = rb.getId();
}
if (checkedCount > 1) {
MyRadioGroup.this.removeAllViews();
}
mRadioButtons.put(id, rb);
}
/**
* {@inheritDoc}
*/
public void onChildViewAdded(View parent, View child) {
if (parent == MyRadioGroup.this && child instanceof RadioButton) {
setIdAndOnCheckedChangeListener((RadioButton) child);
} else if (parent == MyRadioGroup.this && child instanceof ViewGroup) {
ArrayList<RadioButton> hs = findRadioButtons((ViewGroup) child);
for (RadioButton rb : hs) {
setIdAndOnCheckedChangeListener(rb);
}
}
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(parent, child);
}
}
/**
* {@inheritDoc}
*/
public void onChildViewRemoved(View parent, View child) {
if (parent == MyRadioGroup.this && child instanceof RadioButton) {
((RadioButton) child).setOnCheckedChangeListener(null);
}
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
}
}
3.新建ColourImageView.java页面,重写AppCompatImageView控件,鸿洋大神源码为重写ImageView,因为一直报错,所以用了这个,不知道后来出现的问题是不是和这个有关。
ColourImageView.java页面:
package com.example.weixu.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.support.v7.widget.AppCompatImageView;
import android.util.Log;
import android.view.MotionEvent;
import com.example.weixu.munal.R;
import java.util.Random;
import java.util.Stack;
/**
* Created by weixu on 2017/4/8.
*/
public class ColourImageView extends AppCompatImageView {
private int myColor; //接收选择的颜色
private boolean myColorType; //判断是否选择颜色,选择为true,未选择为false
public void setMyColor(int myColor) {
this.myColor = myColor;
}
public void setMyColorType(boolean myColorType) {
this.myColorType = myColorType;
}
private Bitmap mBitmap;
//边界颜色
private int mBorderColor = -1;
private boolean hasBorderColor = false;
private Stack<Point> mStacks = new Stack<Point>(); //栈
public ColourImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ColourImageView); //用于自定义属性
mBorderColor = ta.getColor(R.styleable.ColourImageView_border_color, -1);
hasBorderColor = (mBorderColor != -1);
ta.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int viewWidth = getMeasuredWidth();
//以宽度为标准,等比例缩放view的高度
setMeasuredDimension(viewWidth,
getDrawable().getIntrinsicHeight() * viewWidth / getDrawable().getIntrinsicWidth());
//根据drawable,去得到一个和view一样大小的bitmap
BitmapDrawable drawable = (BitmapDrawable) getDrawable();
Bitmap bm = drawable.getBitmap();
mBitmap = Bitmap.createScaledBitmap(bm, getMeasuredWidth(), getMeasuredHeight(), false);
}
@Override
public boolean onTouchEvent(MotionEvent event)
{
final int x = (int) event.getX();
final int y = (int) event.getY();
if (event.getAction() == MotionEvent.ACTION_DOWN)
{
//填色
fillColorToSameArea(x, y);
}
return super.onTouchEvent(event);
}
/**
* 根据x,y获得该点颜色,进行填充 *
* @param x
* @param y
*/
private void fillColorToSameArea(int x, int y)
{
Bitmap bm = mBitmap;
int pixel = bm.getPixel(x, y);
if (pixel == Color.TRANSPARENT || (hasBorderColor && mBorderColor == pixel))
{
return;
}
int newColor;
if(myColorType)
newColor=myColor;
else
newColor = randomColor();
int w = bm.getWidth();
int h = bm.getHeight();
//拿到该bitmap的颜色数组
int[] pixels = new int[w * h];
bm.getPixels(pixels, 0, w, 0, 0, w, h);
//填色
fillColor(pixels, w, h, pixel, newColor, x, y);
//重新设置bitmap
bm.setPixels(pixels, 0, w, 0, 0, w, h);
setImageDrawable(new BitmapDrawable(bm));
}
/**
* @param pixels 像素数组
* @param w 宽度
* @param h 高度
* @param pixel 当前点的颜色
* @param newColor 填充色
* @param i 横坐标
* @param j 纵坐标
*/
private void fillColor(int[] pixels, int w, int h, int pixel, int newColor, int i, int j)
{
//步骤1:将种子点(x, y)入栈;
mStacks.push(new Point(i, j));
//步骤2:判断栈是否为空,
// 如果栈为空则结束算法,否则取出栈顶元素作为当前扫描线的种子点(x, y),
// y是当前的扫描线;
while (!mStacks.isEmpty())
{
/**
* 步骤3:从种子点(x, y)出发,沿当前扫描线向左、右两个方向填充,
* 直到边界。分别标记区段的左、右端点坐标为xLeft和xRight;
*/
Point seed = mStacks.pop();
//L.e("seed = " + seed.x + " , seed = " + seed.y);
int count = fillLineLeft(pixels, pixel, w, h, newColor, seed.x, seed.y);
int left = seed.x - count + 1;
count = fillLineRight(pixels, pixel, w, h, newColor, seed.x + 1, seed.y);
int right = seed.x + count;
/**
* 步骤4:
* 分别检查与当前扫描线相邻的y - 1和y + 1两条扫描线在区间[xLeft, xRight]中的像素,
* 从xRight开始向xLeft方向搜索,假设扫描的区间为AAABAAC(A为种子点颜色),
* 那么将B和C前面的A作为种子点压入栈中,然后返回第(2)步;
*/
//从y-1找种子
if (seed.y - 1 >= 0)
findSeedInNewLine(pixels, pixel, w, h, seed.y - 1, left, right);
//从y+1找种子
if (seed.y + 1 < h)
findSeedInNewLine(pixels, pixel, w, h, seed.y + 1, left, right);
}
}
/**
* 在新行找种子节点
*
* @param pixels
* @param pixel
* @param w
* @param h
* @param i
* @param left
* @param right
*/
private void findSeedInNewLine(int[] pixels, int pixel, int w, int h, int i, int left, int right)
{
/**
* 获得该行的开始索引
*/
int begin = i * w + left;
/**
* 获得该行的结束索引
*/
int end = i * w + right;
boolean hasSeed = false;
int rx = -1, ry = -1;
ry = i;
/**
* 从end到begin,找到种子节点入栈(AAABAAAB,则B前的A为种子节点)
*/
while (end >= begin)
{
if (pixels[end] == pixel)
{
if (!hasSeed)
{
rx = end % w;
mStacks.push(new Point(rx, ry));
hasSeed = true;
}
} else
{
hasSeed = false;
}
end--;
}
}
/**
* 往右填色,返回填充的个数
*
* @return
*/
private int fillLineRight(int[] pixels, int pixel, int w, int h, int newColor, int x, int y)
{
int count = 0;
while (x < w)
{
//拿到索引
int index = y * w + x;
if (needFillPixel(pixels, pixel, index))
{
pixels[index] = newColor;
count++;
x++;
} else
{
break;
}
}
return count;
}
/**
* 往左填色,返回填色的数量值
*
* @return
*/
private int fillLineLeft(int[] pixels, int pixel, int w, int h, int newColor, int x, int y)
{
int count = 0;
while (x >= 0)
{
//计算出索引
int index = y * w + x;
if (needFillPixel(pixels, pixel, index))
{
pixels[index] = newColor;
count++;
x--;
} else
{
break;
}
}
return count;
}
private boolean needFillPixel(int[] pixels, int pixel, int index)
{
if (hasBorderColor)
{
return pixels[index] != mBorderColor;
} else
{
return pixels[index] == pixel;
}
}
/**
* 产生随机颜色
* @return
*/
public int randomColor(){
Random random = new Random();
int color = Color.argb(255, random.nextInt(256), random.nextInt(256), random.nextInt(256));
return color;
}
}
4.activity_main.xml页面:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context="com.example.weixu.munal.MainActivity">
<com.example.weixu.view.MyRadioGroup
android:layout_marginTop="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rbRed"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="changeColor"
android:text="红色" />
<RadioButton
android:id="@+id/rbOrange"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="changeColor"
android:text="橙色" />
<RadioButton
android:id="@+id/rbYellow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="changeColor"
android:text="黄色" />
<RadioButton
android:id="@+id/rbGreen"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="changeColor"
android:text="绿色" />
<RadioButton
android:id="@+id/rbCyan"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="changeColor"
android:text="青色" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rbBlue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="changeColor"
android:text="蓝色" />
<RadioButton
android:id="@+id/rbPurple"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="changeColor"
android:text="紫色" />
<RadioButton
android:id="@+id/rbBrown"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="changeColor"
android:text="棕色" />
<RadioButton
android:id="@+id/rbBlack"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="changeColor"
android:text="黑色" />
<RadioButton
android:id="@+id/rbRandom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="changeColor"
android:checked="true"
android:text="随机" />
</LinearLayout>
</com.example.weixu.view.MyRadioGroup>
<com.example.weixu.view.ColourImageView
android:id="@+id/civColorIn"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:srcCompat="@drawable/butterfly" />
<Button
android:id="@+id/btClean"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="btClick"
android:text="清空" />
</LinearLayout>
5.MainActivity.java页面:
package com.example.weixu.munal;
import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import com.example.weixu.view.ColourImageView;
import java.util.Random;
public class MainActivity extends AppCompatActivity {
private ColourImageView civColorIn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
civColorIn.setMyColorType(false); //默认为false,只有选择颜色财位true
}
private void init() {
civColorIn= (ColourImageView) findViewById(R.id.civColorIn);
}
public void changeColor(View view){
switch (view.getId()){
case R.id.rbRed:
setColor(255,255,0,0);
break;
case R.id.rbOrange:
setColor(255,255,125,0);
break;
case R.id.rbYellow:
setColor(255,255,255,0);
break;
case R.id.rbGreen:
setColor(255,0,255,0);
break;
case R.id.rbCyan:
setColor(255,0,255,255);
break;
case R.id.rbBlue:
setColor(255,0,255,255);
break;
case R.id.rbPurple:
setColor(255,255,0,255);
break;
case R.id.rbBrown:
setColor(255,125,100,10);
break;
case R.id.rbBlack:
setColor(255,0,0,0);
break;
case R.id.rbRandom:
civColorIn.setMyColorType(false); //选择随机则传递false
break;
}
}
public void btClick(View view){
switch (view.getId()){
case R.id.btClean:
civColorIn.setImageResource(R.drawable.butterfly); //清空
break;
}
}
//如果选择了颜色,传递选中的颜色,并设置为true
public void setColor(int a,int r,int g,int b){
civColorIn.setMyColor(Color.argb(a,r,g,b));
civColorIn.setMyColorType(true);
}
}
源码:
点击下载