前言
这已经是Android UI 绘制过程浅析系列文章的第五篇了,不出意外的话也是最后一篇。再次声明一下,这一系列文章,是我在拜读了csdn大牛的博客文章《》后进行的实践。
前面依次了解了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
布局文件画出来是这个样子的:
接下来是对应的组合控件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"); } }); }}
效果很简单,就不截图了。
继承控件
继承控件在保留原控件全部功能的基础上,添加了新的特性。郭霖大神在《》中举了个继承ListView的例子,我觉得非常好,这里借鉴一下。
在手机QQ(v5.8.0)的会话列表,每一条目都可以向左滑动,出现操作菜单,比起长按出现删除菜单,是更加快捷友好的方式。如下
这里我们首先创建一个操作按钮的布局。
operate_buttons.xml
截图如下
创建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{ public SlideOperateAdapter(Context context, int resource, List 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
最后是主Activity,在布局文件中使用SlideOperateListView,在Activity中为它设置一个初始化数据过的Adapter。
这里只是用toast处理了performTop、performDelete的效果,如果要更进一步,可以在这两个地方调整list中的数据,然后调用adapter.notifyDataSetChanged,即可看到仿真的置顶/删除效果。
fake_main_activity.xml
FakeMainActivity.java
public class FakeMainActivity extends Activity { private SlideOperateListView slideOperateListView; private SlideOperateAdapter slideOperateAdapter; private ListslideOperateList = 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是一个博大精深的系统,自己仍然有很多东西要学,路漫漫其修远兮,吾将上下而求索。