這已是Android UI 繪製過程淺析系列文章的第五篇了,不出意外的話也是最後一篇。再次聲明一下,這一系列文章,是我在拜讀了csdn大牛郭霖的博客文章《帶你一步步深刻了解View》後進行的實踐。java
前面依次瞭解了inflate的過程,以及繪製View的三個步驟:measure, layout, draw。這一次來親身實踐一下,經過自定義View來加深對這幾個過程的理解。android
根據實現方式,自定義View能夠分爲如下3種類型。canvas
自繪控件須要咱們實現onDraw的繪製方法。這裏作了一個小demo,RockPaperScissorView。當用戶點擊View時,隨機出現石頭/布/剪刀中的一種手勢。爲了簡化,沒有采用圖片展現,而是用的文字。dom
RockPaperScissorView.javaide
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)。首先是佈局文件。this
simple_contact.xmlspa
<?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>
佈局文件畫出來是這個樣子的:.net
接下來是對應的組合控件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是一個博大精深的系統,本身仍然有不少東西要學,路漫漫其修遠兮,吾將上下而求索。