Android UI 绘制过程浅析(五)自定义View
前言
这已经是Android UI 绘制过程浅析系列文章的第五篇了,不出意外的话也是最后一篇。再次声明一下,这一系列文章,是我在拜读了csdn大牛郭霖的博客文章《带你一步步深入了解View》后进行的实践。
前面依次了解了inflate的过程,以及绘制View的三个步骤:measure, layout, draw。这一次来亲身实践一下,通过自定义View来加深对这几个过程的理解。
自定义View的分类
根据实现方式,自定义View可以分为以下3种类型。
- 自绘控件。View的绘制代码(onDraw)由开发者自己完成。
- 组合控件。类似Java中的组合,将SDK提供的多个View合成为一个。
- 继承控件。类似Java中的继承,为SDK的某个控件增添新的功能。
自绘控件
自绘控件需要我们实现onDraw的绘制方法。这里做了一个小demo,RockPaperScissorView。当用户点击View时,随机出现石头/布/剪刀中的一种手势。为了简化,没有采用图片展示,而是用的文字。
RockPaperScissorView.java
public class RockPaperScissorView extends View implements View.OnClickListener {
private Paint mPaint;
private static final String[] GESTURES = {"Rock", "Paper", "Scissor"};
private Random rand = new Random(System.currentTimeMillis());
private String mText;
private Rect mBounds; public RockPaperScissorView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBounds = new Rect();
mText = "click me plz...";
super.setOnClickListener(this);
} @Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.GREEN); // 背景色
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
mPaint.setColor(Color.RED);
mPaint.setTextSize(100); // 文字颜色、大小
mPaint.getTextBounds(mText, 0, mText.length(), mBounds);
float textWidth = mBounds.width();
float textHeight = mBounds.height();
canvas.drawText(mText, getWidth() / 2 - textWidth / 2, getHeight() / 2 + textHeight / 2, mPaint);
} private void setText (String s) {
mText = s;
super.invalidate();
} @Override
public void onClick(View v) {
setText(GESTURES[rand.nextInt(GESTURES.length)]);
}
自定义View需要实现onClickListener接口,不要忘了在构造函数中setOnClickListener(this)。在Canvas.drawText中,参数决定的开始绘制的点是文本的左下角,故通过 canvas.drawText(mText, getWidth()/2 - textWidth/2, getHeight()/2 + textHeight/2, mPaint) 来控制居中。截图如下:(动图技能尚未get)
组合控件
SDK提供了Button、TextView、ImageView等等一系列基础的控件,当我们需要一个比较复杂且通用的控件时,可以将这些基础控件组装起来,构成自己的组合控件。
下面实现一个简单的小demo,实现了通讯录联系人的一行样式,包含头像(ImageView)、姓名(TextView)、电话号码(TextView)。首先是布局文件。
simple_contact.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/sky_blue"
android:padding="10dp"> <ImageView
android:id="@+id/avatar"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:src="@drawable/liangjingru" /> <TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/avatar"
android:layout_marginLeft="6dp"
android:layout_toRightOf="@id/avatar"
android:text="梁静茹"
android:textColor="@color/black"
android:textSize="@dimen/text_size_34" /> <TextView
android:id="@+id/phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/name"
android:layout_below="@id/name"
android:layout_marginTop="10dp"
android:text="093132520"
android:textColor="@color/black"
android:textSize="@dimen/text_size_24" /> </RelativeLayout>
布局文件画出来是这个样子的:
接下来是对应的组合控件View文件,提供了三个自定义的方法,用来分别设置头像、姓名、手机号。
SimpleContactView.java
public class SimpleContactView extends FrameLayout {
private ImageView ivAvatar;
private TextView tvName;
private TextView tvPhone; public SimpleContactView(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.simple_contact_view, this);
ivAvatar = (ImageView) super.findViewById(R.id.avatar);
tvName = (TextView) super.findViewById(R.id.name);
tvPhone = (TextView) super.findViewById(R.id.phone);
} public void setAvatar(int resourceId) {
ivAvatar.setImageResource(resourceId);
super.invalidate();
} public void setName(String name) {
tvName.setText(name);
super.invalidate();
} public void setPhone(String phone) {
tvPhone.setText(phone);
super.invalidate();
}
}
在使用SimpleContactView的地方,可以直接调用setAvatar/setName/setPhone来修改联系人信息。这里我们实现的效果是,当点击View时,把梁静茹换为孙燕姿 :)
FakeMainActivity.java
public class FakeMainActivity extends Activity {
private SimpleContactView simpleContactView; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.setContentView(R.layout.fake_main_activity);
simpleContactView = (SimpleContactView) super.findViewById(R.id.simple_contact);
simpleContactView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
simpleContactView.setAvatar(R.drawable.sunyanzi);
simpleContactView.setName("孙燕姿");
simpleContactView.setPhone("5080309921");
}
});
}
}
效果很简单,就不截图了。
继承控件
继承控件在保留原控件全部功能的基础上,添加了新的特性。郭霖大神在《带你一步步深入了解View(四)》中举了个继承ListView的例子,我觉得非常好,这里借鉴一下。
在手机QQ(v5.8.0)的会话列表,每一条目都可以向左滑动,出现操作菜单,比起长按出现删除菜单,是更加快捷友好的方式。如下
这里我们首先创建一个操作按钮的布局。
operate_buttons.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"> <TextView
android:layout_width="80dp"
android:layout_height="50dp"
android:background="@color/green"
android:gravity="center"
android:text="置顶"
android:textColor="@color/white" /> <TextView
android:layout_width="80dp"
android:layout_height="50dp"
android:background="@color/red"
android:gravity="center"
android:text="删除"
android:textColor="@color/white" /> </LinearLayout>
截图如下
创建SlideOperateListView.java,继承ListView.java。需要实现OnTouchListener接口,在onTouch方法中收起菜单(譬如下滑列表、点击某一列的操作)。实现OnGestureListener接口,在onDown方法中获取到用户点击的item,在onFling方法中展示菜单。
在SlideOperateListView中还声明了回调接口OperateListener,使用到的地方必须实现这个接口,内含performTop、performDelete两个方法。
SlideOperateListView.java
public class SlideOperateListView extends ListView implements View.OnTouchListener, GestureDetector.OnGestureListener { private GestureDetector gestureDetector;
private OperateListener operateListener; private View vOperateMenu;
private ViewGroup itemLayout;
private View btnTop, btnDelete; private int selectedItem; private boolean operateMenuShown; public SlideOperateListView(Context context, AttributeSet attrs) {
super(context, attrs);
gestureDetector = new GestureDetector(context, this);
setOnTouchListener(this); } public void setOperateListener(OperateListener operateListener) {
this.operateListener = operateListener;
} @Override
public boolean onTouch(View v, MotionEvent event) {
if (operateMenuShown) {
itemLayout.removeView(vOperateMenu);
operateMenuShown = false;
return false;
} else {
return gestureDetector.onTouchEvent(event);
}
} @Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (!operateMenuShown && Math.abs(velocityX) > Math.abs(velocityY)) {
if (vOperateMenu == null) {
vOperateMenu = LayoutInflater.from(getContext()).inflate(R.layout.operate_buttons, this, false);
}
if (btnTop == null) {
btnTop = vOperateMenu.findViewById(R.id.top_btn);
btnTop.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
itemLayout.removeView(vOperateMenu);
operateListener.performTop(selectedItem);
operateMenuShown = false;
}
});
}
if (btnDelete == null) {
btnDelete = vOperateMenu.findViewById(R.id.delete_btn);
btnDelete.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
itemLayout.removeView(vOperateMenu);
operateListener.performDelete(selectedItem);
operateMenuShown = false;
}
});
}
itemLayout = (ViewGroup) getChildAt(selectedItem - getFirstVisiblePosition());
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
params.addRule(RelativeLayout.CENTER_VERTICAL);
itemLayout.addView(vOperateMenu, params);
operateMenuShown = true;
}
return false;
} @Override
public void onLongPress(MotionEvent e) { } @Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
} @Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
} @Override
public void onShowPress(MotionEvent e) { } @Override
public boolean onDown(MotionEvent e) {
if (!operateMenuShown) {
selectedItem = pointToPosition((int) e.getX(), (int) e.getY());
}
return false;
} public interface OperateListener {
void performTop(int idx);
void performDelete(int idx);
}
}
接下来是ListView对应的SlideOperateAdapter,继承了最简单的ArrayAdapter<String>,布局文件也一起贴在下面。
注意布局文件里根节点是RelativeLayout,与上面SlideOperateListView中addView所声明的params对应。
SlideOperateAdapter.java
public class SlideOperateAdapter extends ArrayAdapter<String> {
public SlideOperateAdapter(Context context, int resource, List<String> objects) {
super(context, resource, objects);
} @Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.slide_operate_list_view_item, null);
}
((TextView) convertView.findViewById(R.id.text)).setText(getItem(position));
return convertView;
}
}
slide_operate_list_view_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"> <TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:gravity="center_vertical"
android:text="item ?" /> </RelativeLayout>
最后是主Activity,在布局文件中使用SlideOperateListView,在Activity中为它设置一个初始化数据过的Adapter。
这里只是用toast处理了performTop、performDelete的效果,如果要更进一步,可以在这两个地方调整list中的数据,然后调用adapter.notifyDataSetChanged,即可看到仿真的置顶/删除效果。
fake_main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fake_main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"> <com.leili.imhere.view.SlideOperateListView
android:id="@+id/contacts"
android:layout_width="match_parent"
android:layout_height="match_parent" /> </FrameLayout>
FakeMainActivity.java
public class FakeMainActivity extends Activity {
private SlideOperateListView slideOperateListView;
private SlideOperateAdapter slideOperateAdapter;
private List<String> slideOperateList = new ArrayList<>(); @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.setContentView(R.layout.fake_main_activity);
initData();
slideOperateListView = (SlideOperateListView) super.findViewById(R.id.contacts);
slideOperateListView.setOperateListener(new SlideOperateListView.OperateListener() {
@Override
public void performTop(int idx) {
ViewUtils.toast(FakeMainActivity.this, idx + " top!");
} @Override
public void performDelete(int idx) {
ViewUtils.toast(FakeMainActivity.this, idx + " delete!"); }
});
slideOperateAdapter = new SlideOperateAdapter(this, 0, slideOperateList);
slideOperateListView.setAdapter(slideOperateAdapter);
} private void initData() {
slideOperateList.add("Item 0");
slideOperateList.add("Item 1");
slideOperateList.add("Item 2");
slideOperateList.add("Item 3");
slideOperateList.add("Item 4");
slideOperateList.add("Item 5");
slideOperateList.add("Item 6");
slideOperateList.add("Item 7");
slideOperateList.add("Item 8");
slideOperateList.add("Item 9");
slideOperateList.add("Item 10");
slideOperateList.add("Item 11");
slideOperateList.add("Item 12");
slideOperateList.add("Item 13");
slideOperateList.add("Item 14");
slideOperateList.add("Item 15");
}
}
最后截图如下
小结
至此为止,五篇 《Android UI 绘制过程浅析》已经全部写好了,自觉对这部分知识的认识尚很粗浅,难免有疏漏不当之处。希望这几篇文章在给朋友们提供一些参考的同时,能够收到改进的建议。写完后,由衷觉得Android是一个博大精深的系统,自己仍然有很多东西要学,路漫漫其修远兮,吾将上下而求索。
Android UI 绘制过程浅析(五)自定义View的更多相关文章
- Android UI 绘制过程浅析(四)draw过程
前言 draw是绘制View三个步骤中的最后一步.同measure.layout一样,通常不对draw本身进行重写,draw内部会调用onDraw方法,子类View需要重写onDraw(Canvas) ...
- Android UI 绘制过程浅析(二)onMeasure过程
前言 View的绘制过程分为 measure.layout.draw三个步骤,接下来对这三个步骤逐一进行研究. measure方法的签名 public final void measure(int w ...
- Android UI 绘制过程浅析(一)LayoutInflater简介
前言 这篇blog是我在阅读过csdn大牛郭霖的<带你一步步深入了解View>一系列文章后,亲身实践并做出的小结.作为有志向的前端开发工程师,怎么可以不搞懂View绘制的基本原理——简直就 ...
- Android UI 绘制过程浅析(三)layout过程
前言 上一篇blog中,了解到measure过程对View进行了测量,得到measuredWidth/measuredHeight.对于ViewGroup,则计算出全部children的宽高进行求和. ...
- Android绘图机制(二)——自定义View绘制形, 圆形, 三角形, 扇形, 椭圆, 曲线,文字和图片的坐标讲解
Android绘图机制(二)--自定义View绘制形, 圆形, 三角形, 扇形, 椭圆, 曲线,文字和图片的坐标讲解 我们要想画好一些炫酷的View,首先我们得知道怎么去画一些基础的图案,比如矩形,圆 ...
- Android绘图机制(一)——自定义View的基础属性和方法
Android绘图机制(一)--自定义View的基础属性和方法 自定义View看起来,确实看起来高深莫测,很多Android开发都不是特别在行这一块,这里面的逻辑以及一些绘画都是有一点难的,说一下我目 ...
- Android绘图机制(三)——自定义View的实现方式以及半弧圆新控件
Android绘图机制(三)--自定义View的三种实现方式以及实战项目操作 在Android绘图机制(一)--自定义View的基础属性和方法 里说过,实现自定义View有三种方式,分别是 1.对现有 ...
- Android开发之制作圆形头像自定义View,直接引用工具类,加快开发速度。带有源代码学习
作者:程序员小冰,CSDN博客:http://blog.csdn.net/qq_21376985 QQ986945193 博客园主页:http://www.cnblogs.com/mcxiaobing ...
- Android UI绘制流程及原理
一.绘制流程源码路径 1.Activity加载ViewRootImpl ActivityThread.handleResumeActivity() --> WindowManagerImpl.a ...
随机推荐
- Xcode添加代码块
1.在编辑区写好代码 @property (nonatomic, assign) <#type#> <#name#> 在Xcode中实际输入完成之后显示的是下面的样子: 2.全 ...
- node.js Websocket实现扫码二维码登录---GoEasy
最近在做一个扫码登录功能,为此我还在网上搜了一下关于微信的扫描登录的实现方式.当这个功能完成了后,我决定将整个实现思路整理出来,方便自己以后查看也方便其他有类似需求的程序猿些. 要实现扫码登录我们需要 ...
- SQL中跨服务器查询
1.跨库查询 select * from [库名].dbo.表名 2.跨库查询 select * from OPENDATASOURCE('SQLOLEDB','Data Source=服务器名或IP ...
- web安全之sql注入的防御
自动把引号转义 1.防御sql注入的基本原则 任何时候不应该改变用户的输入 比如用户输入单引号,那输出也要是单引号. 几种基本的防 ...
- cannot simultaneously fetch multiple bags
问题是什么时候出现的呢? 当一个实体对象中包含多于一个non-lazy获取策略时,比如@OneToMany,@ManyToMany或者@ElementCollection时,获取策略为(fetc ...
- CodeForces 239 Long Path
每个房间有两个单向出口,就是只能进不能出,这个开始理解错了. 进入房间的时候,首先要在屋顶画一个叉叉,如果画完之后叉叉的个数是奇数的话:那么就从第二条出口出去,会到达p[ i ]房间:如果叉叉的个数是 ...
- 兼容ie7、8、9、10、FF、Chrome的遮罩显示
经常碰到这种情形,要实现图片上有一层遮罩和按钮,鼠标滑过时遮罩颜色变深且按钮图片变化,磕磕碰碰终于弄出来题目所述兼容的解决方案. 对于遮罩的实现,将遮罩层.按钮.图片放置在同一个div中,根据abso ...
- java疑问-继承问题
存在两个类,B 继承 A,C 继承 B,我们能将 B 转换为 C 么?如 C = (C) B:
- label标签使用过程中遇到的问题
最近在做东西时候使用到了label标签,由于之前很少使用label标签,所以就遇到一个很奇怪的问题.什么问题呢?下面来看一个效果 <!DOCTYPE html> <html> ...
- js取整函数
1.丢弃小数部分,保留整数部分parseInt(5/2) 2.向上取整,有小数就整数部分加1 Math.ceil(5/2) 3,四舍五入. Math.round(5/2) 4,向下取整 Math.fl ...