3 Android 控件架构和自定义控件详解
3.1 Android 控件架构
- 控件分类
- View
- ViewGroup
- ViewGroup 和 View 的关系
- ViewGroup 可以包含多个view,并且管理包含的view控件,通过ViewGroup,整个界面形成了一个树形结构(空间树)上层控件负责下层子控件的测量与绘制,并传递交互事件
- 通常在activity中调用findViewById()来对控件树进行深度优先遍历来查找对应的元素
- 在每一棵控件树的顶部都存在一个ViewParent对象,所有的交互事件都由它统一调度和分配
View 的测量
- 测量的三种模式
- EXACTLY 这种测量模式是在指定了控件大小之后启用的
- AT_MOST 这种测量模式实在指定控件宽或高为wrap_content 的时候启用的
- UNSPECIFIED 这种状态没有具体的使用过不清楚
View类默认只支持EXACTLY模式,如果想要view支持AT_MOST,那么就要重写onMeasure()方法进行测量
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //在onMeasure的父方法中调用的就是setMeasuredDimension(measureWidth,measureHeight);所以在这里直接调用 setMeasuredDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec)); } /** * 测量宽 widthMeasureSpec是一个32位的值 其中高2位为测量模式,低30位为测量的大小,分别通过MeasureSpec.getMode() * & MeasureSpec.getSize() 分别来获取 * @param widthMeasureSpec * @return */ private int measureWidth(int widthMeasureSpec){ //从measureSpec中获取mode & value int mode = MeasureSpec.getMode(widthMeasureSpec); int size = MeasureSpec.getSize(widthMeasureSpec); //判断测量模式 if(mode == MeasureSpec.EXACTLY){ //这个模式有多大就是多大 return size; }else if(mode == MeasureSpec.AT_MOST){ //这个就是为控件添加了wrap_content值 那么就要和父控件的大小进行比较了 int parentSize = 100; return size > parentSize ? parentSize : size; } return size; } /** * 测量高 同样可以通过测量width的方式来进行 * @param heightMeasureSpec * @return */ private int measureHeight(int heightMeasureSpec){ //1. 获取测量 mode & size int mode = MeasureSpec.getMode(heightMeasureSpec); int size = MeasureSpec.getSize(heightMeasureSpec); //2. 具体判断测量的模式 if(mode == MeasureSpec.EXACTLY){ //这个模式有多大就是多大 return size; }else if(mode == MeasureSpec.AT_MOST){ //这个就是为控件添加了wrap_content值 那么就要和父控件的大小进行比较了 int parentSize = 100; return size > parentSize ? parentSize : size; } return size; }
layout从测量到显示的步骤
- measure –> layout –> draw –> display
- 测量的三种模式
- View的绘制
- 使用的工具Canvas Paint两个类
- 通过重写onDraw()方法来进行绘图
- 这里的绘制步骤通过后面的2D绘图给出
ViewGroup 的测量
- 通过ViewGroup的特点来看,其下的所有的view都由其管理,那么什么是决定其大小的因素呢,当然是子控件的宽和高,通过遍历所有的子控件的宽高,然后来决定自身的宽高(AT_MOST模式下),其他模式直接指定大小,所有的都可以直接指定大小
ViewGroup 的layout
- 重写ViewGroup的onLayout的方法来决定放置的位置
代码还没有写出来
- 重写ViewGroup的onLayout的方法来决定放置的位置
自定义view
几个重要的回调方法
- onFinishInflate() 从XML加载后返回
- onSizeChanged() 组件大小改变时回调 当然在第一次测量后那么组件大小也是发生了改变,也会调用这个方法
onMeasure() 回调这个方法来进行测量控件的大小和获取测量模式,并且支持wrap_content属性值
onLayout 回调这个方法来控制控件的显示位置(目前为止还不知道怎么使用)
- onTouchEvent() 回调这个方法来处理触摸事件
自定义控件的三种形式
- 继承已有的view 通过重写onDraw()方法来进行扩展
主要来进行修改这个view的一些属性,例如颜色,背景,动画等 通过组合view来实现一个新的viewGroup
通过view的组合来组成一个viewGroup模板,以便以后的多次使用,分下面几步进行在values/ 创建attrs.xml 文件,用来定义自定义的属性集合
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="TopBar"> <attr name="leftButtonColor" format="color|reference"/> <attr name="rightButtonColor" format="color|reference"/> <attr name="TitleText" format="string"/> </declare-styleable> </resources>
使用这些属性,在XML文件中
<com.zh.young.androidreview.custom_control.TopBar android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto" custom:leftButtonColor="@color/colorAccent" custom:rightButtonColor="@color/colorPrimaryDark" custom:TitleText="测试" android:background="@color/colorAccent" android:id="@+id/topbar" />
**这里注意,如果要使用自定义的属性,那么就要引入自定义的命名空间
xmlns:custom=”http://schemas.android.com/apk/res-auto”
除了custom为自定义的命名空间名字外,其他的都是这种形式,无需改变**在自定义的控件的Java程序中获取所有的属性
public void getAttrsCollection(AttributeSet attrs){ ta = getContext().obtainStyledAttributes(attrs, R.styleable.TopBar); topBar_leftButtonColor = ta.getColor(R.styleable.TopBar_leftButtonColor, 0); topBar_rightButtonColor = ta.getColor(R.styleable.TopBar_rightButtonColor, 0); topBar_titleText = ta.getString(R.styleable.TopBar_TitleText); //不要忘记回收这个TypeArray 避免出错 虽然不知道会出现什么错误 ta.recycle(); }
为所有的控件设置属性
private void setAttrs() { leftButton.setBackgroundColor(topBar_leftButtonColor); rightButton.setBackgroundColor(topBar_rightButtonColor); title.setText(topBar_titleText); }
为 Button设置点击事件,使用面向接口编程的思想来进行,这些具体实现由用户来实现
“`
public void setOnButtonClickListener(final OnButtonClickListener onButtonClickListener){
leftButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
onButtonClickListener.leftClick();
}
});rightButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
onButtonClickListener.rightClick();
}
});}
public interface OnButtonClickListener{
void leftClick();
void rightClick();
}
“`- 在绘制弧度时候的发现,弧度的开始位置应该是从水平线的右边开始是0度,然后顺时针增长是正值,逆时针增长是负值,终点的弧度值的确定应该是第一个弧度值加第二个弧度值
自定义ViewGroup实现粘性效果
创建一个ViewGroup继承自ViewGroup
public class CustomNewViewGroup extends ViewGroup {}
重写onMeasure方法,通知子view进行测绘
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int count = getChildCount(); for(int i = 0;i < count;i++){ measureChild(getChildAt(i),widthMeasureSpec,heightMeasureSpec); } }
重写onLayout方法进行放置view
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); //设置自身的宽高 mScreenHeight = getHeight(); MarginLayoutParams layoutParams = (MarginLayoutParams)getLayoutParams(); layoutParams.height = mScreenHeight * count; setLayoutParams(layoutParams); //放置子view,每个占一屏 for(int i = 0;i < count;i++){ View child = getChildAt(i); child.layout(0,i * mScreenHeight,getWidth(),(i+1) * mScreenHeight); } }
重写onTouchEvent()方法支持滑动事件并且重写computeScroll()方法支持scroller的回滑
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN : //滑动的起点记录 mStart = (int) getScaleY(); Log.i(TAG,getScrollY()+""); mLastY = (int) event.getY(); //Log.i(TAG,"已经按下 mLastY = "+mLastY); break; case MotionEvent.ACTION_MOVE : int newY = (int) event.getY(); //Log.i(TAG,"newY : " + newY + "--------------"+"mLastY : " + mLastY); scrollBy(0, mLastY - newY); mLastY = newY; break; case MotionEvent.ACTION_UP : int scroll = (int) (getScrollY() - mStart); Log.i(TAG,"scroll = " + scroll + "----------" + "getScaleY = " + getScrollY()); if(scroll > 0){ if(scroll < mScreenHeight / 3) { //如果滑动的距离小于1/3的屏幕高度,那么弹回当前页 mScroller.startScroll( 0,getScrollY(), 0,-scroll ); Log.i(TAG,"执行了"); }else{ mScroller.startScroll(0,getScrollY(),0,mScreenHeight-scroll); } invalidate(); } break; } return true; } @Override public void computeScroll() { super.computeScroll(); if(mScroller.computeScrollOffset()){ scrollTo(0,mScroller.getCurrY()); postInvalidate(); } }
- 继承已有的view 通过重写onDraw()方法来进行扩展
4 ListView的使用技巧
常用的两种提高ListView效率的两种方法,在适配器中,继承BaseAdapter,主要是getView()方法
- 使用ViewHolder避免每次都进行findviewById(),通过上面的学习,知道Android架构对于每次使用findViewById()都是通过深度优先遍历获取的,这样就增加了系统消耗,所以出现了ViewHolder来避免每次的深度优先的遍历
- 复用View,因为每次的view的创建都要经过measure->layout->draw几个方面,消耗系统资源,所以可以通过复用这些已经申请好的内容来提高系统效率。
- 下面通过代码来看一下用法,这段代码混合了convertView和ViewHolder
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder viewHolder; //作为后面使用的变量,提前声明出来 //如果convertView == null 那么一定是第一次使用,那么就需要做初始化工作,这个初始化工作,就在这个if里面做 if(convertView == null){ convertView = View.inflate(getApplicationContext(),R.layout.listview_item,null); viewHolder = new ViewHolder(); viewHolder.tv_test = (TextView) convertView.findViewById(R.id.tv_test); viewHolder.btn_test = (Button) convertView.findViewById(R.id.btn_test); //将获取奥的控件ID都存入到convertView中,下次就可以直接使用了 convertView.setTag(viewHolder); }else{ //这里就可以从convertView里面讲viewHolder取出来,就避免了再次进行深度遍历了 viewHolder = (ViewHolder) convertView.getTag(); } //然后就可以对view进行操作了 return convertView; } private class ViewHolder{ TextView tv_test; Button btn_test; } }
- 下面通过代码来看一下用法,这段代码混合了convertView和ViewHolder
- 下面是进行ListView的一些修饰工作了,for instance 设置条目的分割线,背景啊,点击的时候的颜色啊等等,是视觉上面的优化
- 设置项目的分割线
- 在XML文件中指定分割线
<ListView
android:divider="@null"
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
这里说一个小知识点:就是@null这个标示,在可以使用reference的属性中(这个reference就是在自定义属性中指定的format=”reference”)可以使用@null表示不使用,例如上面这个属性,还有background等等 - 在Java程序中指定
listView.setDivider(null);
- 在XML文件中指定分割线
- 隐藏LIstView的滚动条
在使用LIstView的时候默认情况下会在右边显示滚动条,如果想不显示,那么就有两种方法
- 在XML文件中指定
android:scrollbars="none"
- 很可惜没有找到在Java代码中如何修改
- 在XML文件中指定
- 取消ListView的Item的点击效果 说白了就是在选中时候的效果
android:listSelector="@color/colorPrimary"
话说我怎么就没点出来颜色呢?!!!!反正都是白的,不知道怎么变颜色 - 设置ListView默认显示在第几项
喏,只能这么设置了
listView.setSelection(4);
//4代表第一个显示的条目,不知道你有没有联想到ViewPager这个工具,一个横向的ListView几乎是,做那什么横着的广告的那个玩意儿,就可以使用类似这样的属性,表示无限循环,就是将第一个显示的位置设置在Integer,max/2的位置,累死你滑不出去,就是这个东西,都是骗人的,哪有无限循环啊,话说你也可以弄个竖着的无线广告,看看你客户的反应,哈哈哈,爽爆
- 动态修改ListView的数据
- 这个举要结合这分页加载这个东东说了,分页加载的意思是,我一次从服务器那二十条数据,显示在客户端(ListView),那么每次这个数据要展示我就要更新数据适配器吧,我拿什么来更新呢?我让你不显示,不让你睡觉!!
myAdapter.notifyDataSetChanged();
当你把数据准备好的时候(你当然是在子线程加载的数据,不然你的主线程会埋怨你的,╮(╯▽╰)╭太难伺候),告诉Handler,我已经准备数据了,你可以进行UI更新了,OK,你就可以调用这个方法了
- 这个举要结合这分页加载这个东东说了,分页加载的意思是,我一次从服务器那二十条数据,显示在客户端(ListView),那么每次这个数据要展示我就要更新数据适配器吧,我拿什么来更新呢?我让你不显示,不让你睡觉!!
- 遍历ListView中的所有Item
如果你想要拿到一些(个)View,那么通过这个方法就可以了
listView.getChildAt(index);
- 如果你的ListView现在是空的,或者是没有网络的情况下加载不出数据,那么为了友好的显示界面,那么你就需要为你的user显示一张可爱的图片,来拉住你user的心,让她往死了拉去刷新(对,平常我都是这么干的!!)
- 在你设置ListView的activity中设置一张图片ImageView,但是这张图片默认是不显示的,你可以试试,原理我还真不知道,如果哪位童鞋知道,请!告!知!满足我这颗渴望的心,下面看代码
- 在XML文件中设置
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/error_image"
/>
- 在Java程序中设置
listView.setEmptyView(findViewById(R.id.image_view));
- 在XML文件中设置
- 在你设置ListView的activity中设置一张图片ImageView,但是这张图片默认是不显示的,你可以试试,原理我还真不知道,如果哪位童鞋知道,请!告!知!满足我这颗渴望的心,下面看代码
- LIstView的滑动监听
- 在医生的书中,看到了这几种监听,手势的监听GestureDetector,滑动速度的监听VelocityTrancker,触摸监听onTouchEvent,滑动监听onScrollListener.我好像现在只会三种╮(╯▽╰)╭,一个是GestureDetector、onTouchEvent,onScrollListener,下面就这几项来介绍一下吧,那么滑动速度还是要补滴,但是好像要吃饭了哎,只能明天更新了,拜~~
- 设置项目的分割线