1.AutoCompleteTextView
1 回顾Android中原生控件
1) autoCompleteTextview 这个控件展示数据和listview一样
2)listview展示数据的原理 : 需要一个适配器(arrayAdapter baseAdapter simpleadapter …)
3)实现代码 completionThreshold=”1”输入一个数就能搜索
3.1)在布局里面声明
<AutoCompleteTextView
android:id="@+id/actv"
android:layout_width="match_parent"
android:completionThreshold="1"
android:layout_height="wrap_content" />
3.2)创建适配器 展示数据
setContentView(R.layout.activity_main);
private static final String[] COUNTRIES = new String[] {
"Belgium", "France", "Italy", "Germany", "Spain"
};
//[1]找到控件
AutoCompleteTextView actv = (AutoCompleteTextView) findViewById(R.id.actv);
//[2]actv这个控件展示数据 原理和listview展示数据的原理一样需要一个适配器
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_dropdown_item_1line, COUNTRIES);
//[3]设置适配器
actv.setAdapter(adapter);
2.button和imagebutton
3.自定义控件的分类
[1]通过Android中原生的控件进行组合 来达到自定义的需求
[2]纯自定义控件
1)定义类继承View
2)定义类继承ViewGroup
[3]View&ViewGroup
view是用户构建界面的基本模块
view在屏幕上占据的是矩形区域
view的职责是绘制和事件的处理
ViewGroup也继承View
ViewGrtoup可以包含自己的孩子
平常我们使用的常见布局 比如 线性 相对 绝对 帧布局都直接继承ViewGroup
4.下拉选择框
功能的分析
1)通过edittext 和 button 和popupWindow 和listview 的一个组合 达到需求
2)当点击按钮 弹出一个popupWindow popupwindow里面展示的内容是由listview来填充
3)当点击listview的条目 把该条目的内容取出来展示到edittext上
4)当点击listview 条目上的删除按钮可以把对应的条目的内容删除
实现步骤
[1]画Ui
<RelativeLayout 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:gravity="center_horizontal"
tools:context=".MainActivity" >
<EditText
android:id="@+id/et_number"
android:layout_width="200dp"
android:layout_height="wrap_content" />
<ImageButton
android:id="@+id/ib_down_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignRight="@id/et_number"
android:background="@null"
android:src="@drawable/down_arrow" />
</RelativeLayout>
[2]根据我们画的ui 写对应的逻辑 首先点击按钮 弹出popupwindow
ListView listview = initListView();
/**
* contentView popupwindw要展示的内容
* width :popupwindo的宽
* true :代表popupwindow可以获取焦点
*/
//[1]初始化popupwindow
if (mpopupWindow == null) {
mpopupWindow = new PopupWindow(listview, et_number.getWidth()-7, 250, true);
//[1.1]点击popupwindow外部消失
mpopupWindow.setOutsideTouchable(true);
mpopupWindow.setBackgroundDrawable(new ColorDrawable());
}
//[2]展示popuowindow 参数1 在哪个view下面 xoff:x轴偏移量
mpopupWindow.showAsDropDown(et_number, 4, -4);
[3]popupwindow实际展示的内容是listview的内容,初始化listview的内容
/**初始化listview的方法**/
private ListView initListView() {
//[1]通过打气筒把一个布局转换成一个listview
ListView listView = (ListView) View.inflate(getApplicationContext(), R.layout.listview, null);
//[]设置listview的分割线
listView.setDivider(new ColorDrawable(Color.GRAY));
listView.setDividerHeight(1);
//[2]listview展示数据 创建适配器
listView.setAdapter(new MyAdapter());
//[3]给listview条目设置点击事件
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
//[4]取出点击条目的内容 数据在哪里 存着就去哪里取
String data = lists.get(position);
//[5]把data的数据展示到edittext上
et_number.setText(data);
//[6]让popupwindow消失
mpopupWindow.dismiss();
}
});
return listView;
}
[4]给listview条目设置点击事件,★ 当listview的条目上有button imageButton checkBox控件的时候会抢占listview条目点击事件的焦点,
解决方案:
1)我们不使用button等控件 换其他控件
2)在listview条目的根上加如下属性
1.android:descendantFocusability="blocksDescendants"
[5] 给listview条目上的删除按钮设置点击事件
ib_delete.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//[5]把点中条目先从集合里面删除
lists.remove(position);
//[6]通知适配器更新
notifyDataSetChanged();
}
});
5.View绘制的流程
测量 ——–> 排版(布局) —–> 绘制
Measure ——->layout——->draw
★测量: 一般我们重写onMeasure方法
measure方法由于是final的所以不可以重写 ,我们发现有一个onMeasure方法
那我就重写onMeasure方法,实际的测量工作是在onMeasure方法里面完成
我们需要在onMeasure方法里面调用setMeasuredDimension 完成控件的测量
/**
* widthMeasureSpec:父容器对myView宽度的期望 (layout_width)
* heightMeasureSpec:父容器对myView高度的期望 (layout_height)
* 参数1和2 实际上是由2部分组成 mode(模式) + size(具体控件大小) 组成
* 模式分三种
* [1]UNSPECIFIED 为指定 爹不管模式 父容器对孩子没有约束
* [2]EXACTLY 精确的 父容器对孩子的大小有限制 布局的参数是一个具体的值 或者match_parent
* [3]AT_MOST 父容器对孩子的最大值有要求 对应布局中wrap_content
*
* 我们在布局中声明的200dp宽和高 是如何传递到onMeasure方法里面的??
* [1]首先孩子像父容器申请200dp的宽layout_width和200dp的高layout_height ,父容器拿着孩子的申请打包成对孩子的期望widthMeasureSpec,heightMeasureSpec.
* 调用孩子的measure方法 ---->会调用onMeasure方法
*
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
if (mode ==MeasureSpec.EXACTLY ) {
//获取大小
int size = MeasureSpec.getSize(widthMeasureSpec);
System.out.println("size:"+size);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
排版 layout
l t r b 参数 如下图
通过layout方法底层调用setFrame方法完成对view的上下左右进行赋值.
当我们在定义一个类继承View 的时候一般不需要重写此方法,因为view摆放的位置是由父容器发起的
draw方法实现对view绘制,我们在实际开发中一般会重写onDraw方法往当前的view上画内容 不会重写draw,先绘制背景,在绘制内容
当绘制内容的时候需要重写onDraw();
6.绘制的实战
1)画线
canvas.drawLine(10, 10, 40, 40, paint);
2)画圆
// 使圆圆滑,没有锯齿
paint.setAntiAlias(true);
// 设置空心圆
paint.setStyle(Style.STROKE);
// 透明
paint.setAlpha(100);
canvas.drawCircle(100, 100, 20, paint);
3)画图片
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
4)画三角形
//设置一个路径 画三角形
Path path = new Path();
int x1=10,y1=20;
int x2=100,y2=100;
int x3=180,y3=20;
//起点
path.moveTo(x1, y1);
//连接点
path.lineTo(x2, y2);
path.lineTo(x3, y3);
path.lineTo(x1, y1);
canvas.drawPath(path, paint);
5)画扇形
// 画扇形 第一个参数:指定一个矩形区域用来展示扇形,第2 和3 参数为角度 第四个参数:true表示显示扇形两边
// false表示隐藏扇形的两边
RectF oval = new RectF(10, 10, 170, 170);
canvas.drawArc(oval, 0, 360, true, paint);
canvas.drawArc(mrectF, 0, -90,true, mPaint);
11-15 07:00:38.140: E/AndroidRuntime(4346): android.view.ViewRootImpl$CalledFromWrongThreadException:
Only the original thread that created a view hierarchy can touch its views. 只有主线程才可以更新ui
调用postInvalidate();
6)动态画圆
在mainActivity 找到自定义控件,然后给控件添加方法调用
//在主线程更新UI调用此方法
invalidate();
//在子线程更新UI调用此方法
postInvalidate();
/** 动态画圆 **/
public void startCircle(final int i) {
new Thread() {
public void run() {
for (int j = 0; j <= i; j++) {
SystemClock.sleep(50);
circle = j;
}
// 在子线程更新UI调用此方法
postInvalidate();
};
}.start();
}
// 画扇形 第一个参数:指定一个矩形区域用来展示扇形,第2 和3 参数为角度 第四个参数:true表示显示扇形两边
// false表示隐藏扇形的两边
RectF oval = new RectF(10, 10, 200, 200);
float sweepAngle = circle / 100f * 360;
canvas.drawArc(oval, 0, sweepAngle, false, paint);
7.开关
需求分析:Android中 原生的开关非常丑陋 ,一般在企业中很少使用,需要我们自己定义view
实现代码
1)在布局中声明我们定义的myView
<RelativeLayout 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"
tools:context=".MainActivity" >
<com.ithiema.toogleview.ToogleView
android:id="@+id/toogleView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
2)在myView构造方法中获取开关背景图片(bitmap) 和 滑动块的背景图片
//[1]先获取开关的背景图片和滑动块的图片(bitmap)
mtoogleViewbg = BitmapFactory.decodeResource(getResources(), R.drawable.toogle_background);
mSlideBg = BitmapFactory.decodeResource(getResources(), R.drawable.toogle_slidebg);
//[2]算出滑块left最大边界值
slideLeftMaxSize = mtoogleViewbg.getWidth() - mSlideBg.getWidth();
3)在移动的方法里面算出移动的距离
float moveX = event.getX();
float distanceX = moveX - downX;
slideLeftPosition+=distanceX;
//[3]对边界进行判断
if (slideLeftPosition <=0) {
slideLeftPosition = 0;
}else if (slideLeftPosition >=slideLeftMaxSize) {
slideLeftPosition = slideLeftMaxSize;
}
downX = moveX;
4)手指抬起的时候出来的逻辑
//当手指抬起时候 滑动块是往左移动还是往右移动
//算出滑动块的中心点位置 = slideLeftPosition + 滑动块宽度的一半 < 背景宽度的一半 就往左移动 否则往右移动
if (slideLeftPosition + mSlideBg.getWidth() /2 < mtoogleViewbg.getWidth()/2 ) {
slideLeftPosition = 0;
}else {
slideLeftPosition = slideLeftMaxSize;
}
5)当手指抬起后 实现开关的功能
if (isHandup) {
isHandup = false;
//[4]获取开关当前的状态
boolean isOpenTemp = slideLeftPosition > 0;
if(isOpen != isOpenTemp && mToogleViewListener!=null){
//[5]条件满足 触发回调方法
mToogleViewListener.toogleState(isOpenTemp);
isOpen = isOpenTemp;
}
}
6)设置开关的状态
public void setToogleViewState(boolean b) {
isHandup = true;
if (b) {
slideLeftPosition = slideLeftMaxSize;
}else {
slideLeftPosition = 0;
}
}
代码:
private ToogleListener toogle;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
// [1]先获取开关的背景图片和滑动块的图片(bitmap)
toogle_background = BitmapFactory.decodeResource(getResources(),
R.drawable.toogle_background);
toogle_slidebg = BitmapFactory.decodeResource(getResources(),
R.drawable.toogle_slidebg);
tooglewidth = toogle_background.getWidth() / 2;
maxX = toogle_background.getWidth() - toogle_slidebg.getWidth();
halfX = maxX / 2;
// 获取自定义的属性
String name = "http://schemas.android.com/apk/res/com.heima.switchd";
boolean attributeBoolean = attrs.getAttributeBooleanValue(name,
"toogleState", false);
setToogleViewState(attributeBoolean);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 这个view大小和背景图片宽高一样
setMeasuredDimension(toogle_background.getWidth(),
toogle_background.getHeight());
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(toogle_background, 0, 0, null);
canvas.drawBitmap(toogle_slidebg, distend, 0, null);
// 判断是抬起状态
if (isUp) {
isUp = false;
// 判断当前的位置
boolean isTemp = distend > 0;
if (isOpen != isTemp && toogle != null) {
// [5]条件满足 触发回调方法
toogle.toogleState(isTemp);
isOpen = isTemp;
}
}
}
// 处理消息
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
// 记录开始的时间
startTime = System.currentTimeMillis();
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX();
// 移动的距离
distend += moveX - downX;
if (distend <= 0) {
distend = 0;
} else if (distend >= maxX) {
distend = maxX;
}
// 移动后再次赋值给初始值
downX = moveX;
break;
case MotionEvent.ACTION_UP:
isUp = true;
// 记录抬起的时间
long upTime = System.currentTimeMillis();
float upX = event.getX();
distend += upX - downX;
long time = upTime - startTime;
if (upX - downX < 5 && time < 200) {
// 时间小于200 是点击事件
if (upX > tooglewidth) {
distend = maxX;
} else {
distend = 0;
}
} else {
// 自动弹回
if (distend <= halfX) {
distend = 0;
} else if (distend > halfX) {
distend = maxX;
}
}
break;
default:
break;
}
invalidate();// --->ondraw方法会执行
return true;
}
/** 监听 **/
public void setOnToogleListener(ToogleListener toogleListener) {
this.toogle = toogleListener;
}
/** 模拟写个接口 包括未实现的方法 **/
public interface ToogleListener {
void toogleState(boolean result);
}
// 设置开关的状态
public void setToogleViewState(boolean b) {
isUp = true;
if (b) {
distend = maxX;
System.out.println("111");
} else {
distend = 0;
System.out.println(2);
}
}
7)自定义开关属性 首先在values下创建attrs文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ToogleView">
<attr name="toogleState" format="boolean" />
</declare-styleable>
</resources>
接下来自定义命名空间
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:itheima="http://schemas.android.com/apk/res/com.ithiema.toogleview"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<com.ithiema.toogleview.ToogleView
android:id="@+id/toogleView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
itheima:toogleState="true"
android:layout_centerInParent="true" />
</RelativeLayout>
在自定义view 的构造方法中初始化
//[3]获取布局中定义的属性值
String namespace = "http://schemas.android.com/apk/res/com.ithiema.toogleview";
boolean toogleState = attrs.getAttributeBooleanValue(namespace, "toogleState", false);
//[4]设置开关的状态
setToogleViewState(toogleState);
8 今天总结
[1]回顾Android原生控件 了解
[2]下拉列表 练习一下
[3]view的绘制流程 ---测量---排版---绘制 掌握
[4]绘制的实战 掌握
[5]开关 掌握
popupwindow 和 listView 组合 下拉选择框
View的绘制流程
1.测量 2.排版(布局) 3.绘制
measure layout draw