梦土上

此博客停止更新,请移步本人网站 http://lilei.work

博客园 首页 新随笔 联系 订阅 管理

前言

  这已经是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是一个博大精深的系统,自己仍然有很多东西要学,路漫漫其修远兮,吾将上下而求索。

posted on 2015-08-29 20:57  猫之歌  阅读(436)  评论(0编辑  收藏  举报