Android自定义控件系列案例【三】

自定义控件的目的有很多,比如系统控件满足不了需求时,我们会想到通过自定义控件来满足需求。其实有的时候为了功能的复用我们也会去自定义控件,把经常要用的或以后要用的与UI相关的功能封装到自定义控件中,让它成为独立的功能,当然为了灵活的控制其中的可变部分,自定义的控件应该预留接口(这里说的接口不是Java中的Interface,是控制可变部分的方式,比如方法之类的)。

接下来的案例用普通的GridView+Adapter也可以实现效果图中的功能,但为了将来复用,我们可以把效果图中的键盘逻辑封装到自定义控件中。

案例描述:

在类似猜成语,猜歌名,猜明星,猜车标,猜电影电视剧等一系列的项目中,都会有一个共同的模块-键盘,用户点击键盘中的文字,代表一个用户输入,其中键盘中包含正确答案和干扰答案,并且一般都是乱序的,而且每一关的干扰答案都不一样,本案例要做的就是实现键盘模块,并通过SeekBar进度的改变模拟不同关卡下键盘上的文字相应的改变(相当于重新洗牌)。

案例效果:


Android自定义控件系列案例【三】_第1张图片
Android自定义控件系列案例【三】_第2张图片

案例实现:

实现方式一 :GridView+Adapter

第一步:布局实现

(1)主界面布局 activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <GridView
        android:id="@+id/gv_keys"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:numColumns="8" />

    <SeekBar
        android:id="@+id/sb_level"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:max="9" />

</RelativeLayout>
(2)按健布局 key_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:id = "@+id/btn_key"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" 
    android:textColor="#1773CE"
    android:textSize = "22sp"
    android:textStyle="bold"
    android:background="@drawable/key_bg">
    

</Button>
其中使用了一个带选择器的背景,drawable/key_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@drawable/word_pressed" android:state_pressed="true"/>
    <item android:drawable="@drawable/word"/>

</selector>
背景选择器中使用了两张图片drawable-hdpi/word.png,drawable-hdpi/word_pressed.png,分别会在普通状态与按下状态显示。

第二步:数据准备:

这块分为两部分,一部分是在XML中配置所有文字和每一关的正确答案,另一部分就是设计工具类,将XML中的文字封装到数据实体类中,然后再把数据实体封装到List中供GridView进行显示。

(1)XML中配置数据

 values/string.xml,其中all_keys对应的值就是所有关卡要用到的文字。
<resources>

    <string name="app_name">InputKeyBoard</string>
    <string name="hello_world">Hello world!</string>
    <string name="action_settings">Settings</string>
    <string name = "all_keys">"应有尽有自吹自擂自说自话百发百中百依百顺百战百胜半信半疑不屈不挠不知不觉不伦不类不折不扣大吹大擂能屈能伸蹑手蹑脚善始善终十全十美惟妙惟肖畏首畏尾无缘无故无影无踪载歌载舞兴高采烈怒气冲冲聚精会神自言自语千钧一发雨后春笋琳琅满目顶天立地千方百计小心翼翼焕然一新胸有成竹草木皆兵赤膊上阵用兵如神目不转睛鸿鹄之志一日十行问心无愧"</string>

</resources>
values/arrays.xml,其中一共有10条数据,代表有10关,而每条数据代表每一关的正确答案,将来要保证第一关的键盘上一定有正确答案在,只不过正确答案中的每个字是顺序打乱的。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name = "correntKeys">
      <item>"兴高采烈"</item>
      <item>"怒气冲冲"</item>
      <item>"聚精会神"</item>
      <item>"自言自语"</item>
      <item>"千钧一发"</item>
      <item>"雨后春笋"</item>
      <item>"琳琅满目"</item>
      <item>"顶天立地"</item>
      <item>"千方百计"</item>
      <item>"小心翼翼"</item>  
    </string-array>
</resources>

(2)按键数据实体类设计

数据实体中一共有3条数据,位置,文字,按钮。
package com.kedi.inputkeyboard;

import android.widget.Button;

/**
 * 按键实体类
 * 
 * @author 张科勇
 *
 */
public class Key {
	//按健在键盘中的位置
	private int keyPosition;
	//按键上的文字
	private String keyword;
	//显示按键文字的Button控件
	private Button keyView;
	public Key(){}
	public Key(int keyPosition, String keyword, Button keyView) {
		this.keyPosition = keyPosition;
		this.keyword = keyword;
		this.keyView = keyView;
	}

	public int getKeyPosition() {
		return keyPosition;
	}

	public void setKeyPosition(int keyPosition) {
		this.keyPosition = keyPosition;
	}

	public String getKeyword() {
		return keyword;
	}

	public void setKeyword(String keyword) {
		this.keyword = keyword;
	}

	public Button getKeyView() {
		return keyView;
	}

	public void setKeyView(Button keyView) {
		this.keyView = keyView;
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + keyPosition;
		result = prime * result + ((keyView == null) ? 0 : keyView.hashCode());
		result = prime * result + ((keyword == null) ? 0 : keyword.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Key other = (Key) obj;
		if (keyPosition != other.keyPosition)
			return false;
		if (keyView == null) {
			if (other.keyView != null)
				return false;
		} else if (!keyView.equals(other.keyView))
			return false;
		if (keyword == null) {
			if (other.keyword != null)
				return false;
		} else if (!keyword.equals(other.keyword))
			return false;
		return true;
	}
	
	

}

(3)封装与获取数据工具类设计

  这个工具类的主要作用就是将XML中的字符串数据传进来拆解成每一个字,然后封装到数据实体对象中,并把每个数据实体对象统一保存到List集合中,乱序后返回给调用者。其中在往List集合中存数据实体对象时分了两步,第一步把正确答案对应的数据实体对象保存到List集合中,第二步是从总文字串中随机取出除正确答案外的其它干扰答案,然后将对应的数据实体保存到List集合中,最后如果需要乱序,则调用Collections.shuffle()方法即可。
package com.kedi.inputkeyboard;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

/**
 * 造数据的工具类
 * @author 张科勇
 *
 */
public class DataUtil {
	//总按键数,一般都是确定的
	public static final int TOTAL_KEYS_NUM = 24;
	public static List<Key> getDatas(String allKeys,String correctKeys){
		List<Key> dataList = new ArrayList<Key>();
		//1、封装正确答案对应的Key
		
		 char[] correctKeyChars = correctKeys.toCharArray();
		 for(int i = 0;i<correctKeyChars.length;i++){
			 Key correctKey = new Key();
			 correctKey.setKeyword(correctKeyChars[i]+"");
			 dataList.add(correctKey);
		 }
		//2、封装干扰答案对应的Key
		 //要生成的干扰答案的个数
		char[] allKeyChars = allKeys.toCharArray();
		Random random = new Random();
		while(true){
			int randomInt = random.nextInt(allKeyChars.length);
			char randomkey = allKeyChars[randomInt];
			Key disturbKey = new Key();
			disturbKey.setKeyword(randomkey+"");
			//过滤重复字
			if(!dataList.contains(disturbKey)){
				dataList.add(disturbKey);
			}
			if(dataList.size()==TOTAL_KEYS_NUM){
				break;
			}
		}
		//3.乱序集合
		Collections.shuffle(dataList);
		return dataList;
	}

}

第三步:创建GridView的适配器

这个适配器很简单,就是把上面获取到的List集合数据绑定到键盘按钮上即可。
package com.kedi.inputkeyboard;

import java.util.List;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
/**
 * 键盘控件适器
 * @author 张科勇
 *
 */
public class KeyBoardAdapter extends BaseAdapter {
	//上下文
	private Context mContext;
	//数据集合
	private List<Key> mDatas;

	public KeyBoardAdapter(Context mContext, List<Key> mDatas) {
		this.mContext = mContext;
		this.mDatas = mDatas;
	}

	@Override
	public int getCount() {
		return mDatas.size();
	}

	@Override
	public Object getItem(int position) {
		return mDatas.get(position);
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
			
		    View view = View.inflate(mContext, R.layout.key_layout, null);
			Button keyView = (Button) view.findViewById(R.id.btn_key);
		    Key key = mDatas.get(position);
		    keyView.setText(key.getKeyword());
		    key.setKeyPosition(position);
		    key.setKeyView(keyView);
			
			return view;
	}

}

   第四步:数据获取,适配,以及动态控制

   这部分逻辑都在MainActivity中完成。主要是获取控件,获取数据,适配数据以及SeekBar模拟不同关卡键盘上数据的变化。注释很详细就不再解释了。
package com.kedi.inputkeyboard;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.widget.GridView;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
/**
 * 键盘控制类
 * @author 张科勇
 *
 */
public class MainActivity extends Activity {
//键盘控件
	private GridView mKeyBoardGv;
	//键盘急控件适配器
	private KeyBoardAdapter mAdapter;
	//数据集合
	private List<Key> mDatas = new ArrayList<Key>();
	//拖动条
	private SeekBar mLevelSb;
	// 当前关数
	private int level = 0;
	//所有关对应的正确答案数组,从XML中获得
	private String[] mCorrentKeys;
	//所有关用到的文字库字符串
	private String mAllKeys;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		init();

	}

	private void init() {
		initDatas();
		initViews();
		bindDataToView();
		handleEvent();
	}

	/**
	 * 初始化数据
	 */
	private void initDatas() {
		//从string.xml文件中获取文字库字符串
		mAllKeys = getResources().getString(R.string.all_keys);
		//从arrays.xml中获取所有关对应的正确答案数组,
		mCorrentKeys = getResources().getStringArray(R.array.correntKeys);
		//调用数据工具类,获取当前关对应的数据集合
		mDatas = DataUtil.getDatas(mAllKeys, mCorrentKeys[level]);
	}

	/**
	 * 初始化View
	 */
	private void initViews() {
		mKeyBoardGv = (GridView) findViewById(R.id.gv_keys);
		mLevelSb = (SeekBar) findViewById(R.id.sb_level);
	}

	/**
	 * Data与View绑定
	 */
	private void bindDataToView() {
		mAdapter = new KeyBoardAdapter(this, mDatas);
		mKeyBoardGv.setAdapter(mAdapter);
	}

	/**
	 * 处理交互事件的方法
	 */
	private void handleEvent() {
		mLevelSb.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

			@Override
			public void onStopTrackingTouch(SeekBar seekBar) {

			}

			@Override
			public void onStartTrackingTouch(SeekBar seekBar) {

			}

			@Override
			public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
				level = progress;
				// 获取每一关的数据
				mDatas = DataUtil.getDatas(mAllKeys, mCorrentKeys[level]);
				// 刷新键盘文字
				bindDataToView();
			}
		});
	}

}
上面就是使用Android系统原生控件GridView实现的步骤,接下来使用自定义控件来重构代码。


实现方式二:自定义控件KeyBoardView

首先考虑GridView已经能基本满足我们的界面要求,所以自定键盘控件可以基于GridView进行定制。为了保证自定义控件KeyBoardView尽可能的独立,也就是尽可以的少的依赖其它类,所以打算把之前DataUtil中的功能,KeyBoardAdapter的功能都封装到KeyBoardViewView中,并对外提供获取数据和设置数据接口方法,当然如果以后再有扩展需求,也可以把事件之类的进行封装与回调。

修改处:

1、KeyBoardView自定义控件类。

 这个控件目前没有扩展其它功能,主要是把之前实现的代码基本都移到了这个控件中。

package com.kedi.inputkeyboard;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.GridView;

/**
 * 自定义键盘控件
 * 
 * @author 张科勇
 *
 */
public class KeyBoardView extends GridView {
	// 所有关对应的正确答案数组,从XML中获得
	private String[] mCorrentKeys;
	// 所有关用到的文字库字符串
	private String mAllKeys;
	// 数据集合
	private List<Key> mDatas = new ArrayList<Key>();
	// 键盘急控件适配器
	private KeyBoardAdapter mAdapter;
	// 总按键数,一般都是确定的
	public static final int TOTAL_KEYS_NUM = 24;

	// get方法
	public String[] getCorrentKeys() {
		return mCorrentKeys;
	}

	// get方法
	public String getAllKeys() {
		return mAllKeys;
	}

	public void setDatas(List<Key> mDatas) {
		this.mDatas = mDatas;
		if (mAdapter != null) {
			mAdapter.notifyDataSetChanged();
		}
	}

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

	public KeyBoardView(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public KeyBoardView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		init();
	}

	/**
	 * 初始化方法
	 */
	private void init() {
		initDatas();
		bindDataToView();
	}

	/**
	 * 初始化数据
	 */
	private void initDatas() {
		// 从string.xml文件中获取文字库字符串
		mAllKeys = getResources().getString(R.string.all_keys);
		// 从arrays.xml中获取所有关对应的正确答案数组,
		mCorrentKeys = getResources().getStringArray(R.array.correntKeys);
		// 调用数据工具类,获取当前关对应的数据集合
		mDatas = getDatas(mAllKeys, mCorrentKeys[0]);
	}

	/**
	 * Data与View绑定
	 */
	private void bindDataToView() {
		mAdapter = new KeyBoardAdapter();
		setAdapter(mAdapter);
	}

	/**
	 * 键盘控件适器
	 * 
	 * @author 张科勇
	 *
	 */
	public class KeyBoardAdapter extends BaseAdapter {

		@Override
		public int getCount() {
			return mDatas.size();
		}

		@Override
		public Object getItem(int position) {
			return mDatas.get(position);
		}

		@Override
		public long getItemId(int position) {
			return position;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {

			View view = View.inflate(getContext(), R.layout.key_layout, null);
			Button keyView = (Button) view.findViewById(R.id.btn_key);
			Key key = mDatas.get(position);
			keyView.setText(key.getKeyword());
			key.setKeyPosition(position);
			key.setKeyView(keyView);

			return view;
		}

	}

	/**
	 * 将字符串数据封装成数据实体集合返回
	 * 
	 * @param allKeys
	 *            文字库字符串
	 * @param correctKeys
	 *            每一关正确答案字符串
	 * @return 数据实体集合
	 */
	public List<Key> getDatas(String allKeys, String correctKeys) {
		List<Key> dataList = new ArrayList<Key>();
		// 1、封装正确答案对应的Key

		char[] correctKeyChars = correctKeys.toCharArray();
		for (int i = 0; i < correctKeyChars.length; i++) {
			Key correctKey = new Key();
			correctKey.setKeyword(correctKeyChars[i] + "");
			dataList.add(correctKey);
		}
		// 2、封装干扰答案对应的Key
		// 要生成的干扰答案的个数
		char[] allKeyChars = allKeys.toCharArray();
		Random random = new Random();
		while (true) {
			int randomInt = random.nextInt(allKeyChars.length);
			char randomkey = allKeyChars[randomInt];
			Key disturbKey = new Key();
			disturbKey.setKeyword(randomkey + "");
			// 过滤重复字
			if (!dataList.contains(disturbKey)) {
				dataList.add(disturbKey);
			}
			if (dataList.size() == TOTAL_KEYS_NUM) {
				break;
			}
		}
		// 3.乱序集合
		Collections.shuffle(dataList);
		return dataList;
	}
}

2、activity_main.xml主布局界面。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.kedi.inputkeyboard.KeyBoardView
        android:id="@+id/gv_keys"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:numColumns="8" />

    <SeekBar
        android:id="@+id/sb_level"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:max="9" />

</RelativeLayout>
3、MainActivity类。

之前的与数据,适配等相关的代码都移到自定义控件中,MainActivity类变的轻松很多,可以回头和之前的MainActivity相比一下。

package com.kedi.inputkeyboard;

import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;

/**
 * 使用自定义实现键盘类演示Activity
 * 
 * @author 张科勇
 *
 */
public class MainActivity extends Activity {
	// 键盘控件
	private KeyBoardView mKeyBoardGv;
	// 拖动条
	private SeekBar mLevelSb;
	// 当前关数
	private int level = 0;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mKeyBoardGv = (KeyBoardView) findViewById(R.id.gv_keys);
		mLevelSb = (SeekBar) findViewById(R.id.sb_level);
		mLevelSb.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

			@Override
			public void onStopTrackingTouch(SeekBar seekBar) {

			}

			@Override
			public void onStartTrackingTouch(SeekBar seekBar) {

			}

			@Override
			public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
				level = progress;
				//获取对应关的数据
				List<Key> datas = mKeyBoardGv.getDatas(mKeyBoardGv.getAllKeys(), mKeyBoardGv.getCorrentKeys()[level]);
				//适配当前关键盘
				mKeyBoardGv.setDatas(datas);
			}
		});
	}

}
 尤其是在SeekBar控制的时候,需要的数据什么的都可以从自定义控件中调,而自定控件则不依赖它,做到了相对独立,提高了功能块的复用性。上面案例数据是成语,只要我们愿意,换一下XML中的数据,马上另一个项目的键盘就出来了。比如我按照猜XML中的格式把数据变成车名,其它源代码不用改,运行效果:

Android自定义控件系列案例【三】_第3张图片猜车标之类的项目键盘立马生成。


以上就是使用自定义控件完成代码复用的目的,关于自定义控件的更多功用需要我们继续探索!如想简单了解,可以阅读之前写过的两篇博文:Android自定义控件系列案例【一】、Android自定义控件系列案例【二】









你可能感兴趣的:(android,Android开发,android自定义控件,android项目实战,Android自定义控件系列)