自定义View理解

  • 参考郭霖文章
  • http://blog.csdn.net/guolin_blog/article/details/12921889
    • 1,LayoutInflater的内部实现方式,主要是pull解析布局文件,然后通过反射的方式生成控件对象,形成dom树结构;所以想要设置控件的layout_width 属性必须在该控件外嵌套一个布局才能生效,否则怎样改大小都不会生效的!
    • 2,setContentView方法会给xml布局文件添加一个FrameLayout;
    • 3,视图界面都是由两个部分组成:标题栏和内容栏,标题栏是系统自动给我们添加的,内容栏会自动添加一个根布局framelagout;
    • 这里写图片描述
 setContentView(R.layout.zidingyi_view);


//这里R.layout.button_view,后面是null,只有Button布局,没有父布局,所以layout_width属性怎样设置都是无效的,只能在布局中添加一个父布局才能生效,或者在代码中添加父布局及括号内的写法;
         LayoutInflater lf = LayoutInflater.from(this);
        View buttonView = lf.inflate(R.layout.button_view, null);

(  View buttonView = lf.inflate(R.layout.button_view,  new LinearLayout(this));  )//将null该为父布局!!

        mLayout.addView(buttonView);

        ViewParent viewParent = mLayout.getParent();
        Log.d("gggggg==","这是"+viewParent);//gggggg==: 这是android.support.v7.widget.ContentFrameLayout@41fe9678
  • 深入了解(onMeasure,onLayout,ondraw)
public class MyView extends ViewGroup {//这里是 ViewGroup(容器)
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    //这个方法是确定父视图MyView的大小!
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /*这里可以直接设置该MyView的大小super()改为 setMeasuredDimension(200, 200);尽量不要这么定义
        * 需要注意的是,在setMeasuredDimension()方法调用之后,
        * 我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,
        * 以此之前调用这两个方法得到的值都会是0。
        * 由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,
        * 而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行拍板
        * */
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if(getChildCount() > 0){
            View childView = getChildAt(0);//得到第一个子视图
            measureChild(childView , widthMeasureSpec , heightMeasureSpec);
        }
    }

这里写图片描述

    //这个方法是给视图惊醒布局,确定视图的位置!
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {//左上右下的4个坐标
        if(getChildCount() > 0 ){
            View childView =getChildAt(0);
            //这里面的4个参数就代表这 左(20代表离左边20dp吧) 上     右(代表在onMeasure()得到的宽) 下(得到的高)  数值改变就会改变子视图在父视图MyView的位置
            childView.layout(  20 ,  0 , childView.getMeasuredWidth() , childView.getMeasuredHeight());
        }
        /*
        * 首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,
        * 而getWidth()方法要在layout()过程结束后才能获取到。
        * 另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,
        * 而getWidth()方法中的值则是通过视图右边的坐标 减去 左边的坐标计算出来的。
        *
        * 这里给子视图的layout()方法传入的四个参数分别是
        * 0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),
        * 因此getWidth()方法得到的值就是childView.getMeasuredWidth() - 0 = childView.getMeasuredWidth() ,
        * 所以此时getWidth()方法和getMeasuredWidth() 得到的值就是相同的
        * */
    }
  • 下面就是布局文件
<?xml version="1.0" encoding="utf-8"?>
<com.demo.zhihudemo.test.MyView xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

   <ImageView
       android:id="@+id/dddd_iv"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:src="@drawable/to_up"/>

 //要知道上面只会显示一个布局,下面的都会舍弃!!!
    <com.demo.zhihudemo.test.MyViewTwo
        android:layout_width="200dp"
        android:layout_height="200dp"/>
</com.demo.zhihudemo.test.MyView>
  • 显示结果

这里写图片描述

  • onDraw方法–就直接上代码了,郭神的博客可以去看
public class MyViewTwo extends View {//这是View!!!

    private Paint mPaint;
    public MyViewTwo(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//得到画笔实例,是使位图抗锯齿的标志
    }

    @Override
    protected void onDraw(Canvas canvas) {//自己画图!!!
        mPaint.setColor(Color.WHITE);//父视图的背景颜色
        canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);//父视图的大小,形状rect是矩形!
        mPaint.setColor(Color.BLUE);//这次设置的画笔颜色是为了下一步字体的设置
        mPaint.setTextSize(20);//画笔的大小
        //  第二参数是X轴,第三参数Y轴是总视图的一半的位置 - 100代表往上移动100dp(因为X轴箭头向右为正!Y轴箭头向下为正!)
        canvas.drawText("MyViewTest",  10  , 
        getHeight()/2-100   ,   mPaint);
    }
}
  • 布局文件信息—–借用了之前写的MyView的自定义View
<?xml version="1.0" encoding="utf-8"?>
<com.demo.zhihudemo.test.MyView xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <com.demo.zhihudemo.test.MyViewTwo
        android:layout_width="200dp"
        android:layout_height="200dp"/>
</com.demo.zhihudemo.test.MyView>
  • 显示结果–白色是之前设置的,蓝色是后来设置的!

这里写图片描述

  • 视图状态
    1. enabled
      表示当前视图是否可用。可以调用setEnable()方法来改变视图的可用状态,传入true表示可用,传入false表示不可用。它们之间最大的区别在于,不可用的视图是无法响应onTouch事件的。
  • focused
    表示当前视图是否获得到焦点。通常情况下有两种方法可以让视图获得焦点,即通过键盘的上下左右键切换视图,以及调用requestFocus()方法。而现在的Android手机几乎都没有键盘了,因此基本上只可以使用requestFocus()这个办法来让视图获得焦点了。而requestFocus()方法也不能保证一定可以让视图获得焦点,它会有一个布尔值的返回值,如果返回true说明获得焦点成功,返回false说明获得焦点失败。一般只有视图在focusable和focusable in touch mode同时成立的情况下才能成功获取焦点,比如说EditText。
  • window_focused
    表示当前视图是否处于正在交互的窗口中,这个值由系统自动决定,应用程序不能进行改变。
  • selected
    表示当前视图是否处于选中状态。一个界面当中可以有多个视图处于选中状态,调用setSelected()方法能够改变视图的选中状态,传入true表示选中,传入false表示未选中。
  • pressed
    表示当前视图是否处于按下状态。可以调用setPressed()方法来对这一状态进行改变,传入true表示按下,传入false表示未按下。通常情况下这个状态都是由系统自动赋值的,但开发者也可以自己调用这个方法来进行改变

  • drawable代码文件 —- selector_color.xml —-

<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@color/red" android:state_pressed="true"></item>
    <item android:drawable="@color/yellow" android:state_focused="true"></item>
    <item android:drawable="@color/pink"></item>
    <!--第一个是按下的状态, 第二个是获得焦点的状态,第三个是正常状态-->
</selector>

 <!--添加到布局文件中的背景色中-->
    <Button
        android:id="@+id/btn_test_color"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/selector_color"/>

这里就不上图 ,自己试下,我的是在正常显示的时候是pink颜色,点击之后是red颜色,不太清楚怎么获得focused焦点的状态…..

  • 视图重绘

    • 调用视图的setVisibility()、setEnabled()、setSelected()等方法时都会导致视图重绘,而如果我们想要手动地强制让视图进行重绘,可以调用 invalidate() 方法来实现。
    • 当然了,setVisibility()、setEnabled()、setSelected()等方法的内部其实也是通过调用 invalidate() 方法来实现的

    • 另外需要注意的是,invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measurelayout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()了。这个方法中的流程比invalidate()方法要简单一些,但中心思想是差不多的。

  • 最后是自定义控件—–: 自定义View的实现方式大概可以分为三种,自绘控件、组合控件、以及继承控件。

    • 自绘View
//这个是自绘View的实例
public class MyViewMyself extends View  implements View.OnClickListener{
    private Paint mPaint;
    private Rect mBounds;
    private int mConut;

    public MyViewMyself(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBounds = new Rect();
        //这里记住需要有点击事件.....
        setOnClickListener(this);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);//这个调用不电泳都一样的吧?父类只是一个空方法....
        //第一步 先设置画笔颜色
        mPaint.setColor(Color.YELLOW);
        //第二步 用canvas画布 画出该View的大小,并添加画笔颜色来当做背景色
        canvas.drawRect(0 , 0 ,getWidth(), getHeight(), mPaint);
        //第三步 更改画笔颜色,和字体大小
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(30);
        //第四步 获取文字的宽度和高度
        String  text = String.valueOf(mConut);//数字装换
        //参数一:文字;参数二三:开始的位置到结束的位置(0文字的长度;参数4:文字界限,及文字宽高组成的是一个矩形的..
        mPaint.getTextBounds(text ,0 , text.length() , mBounds);
        //得到文字宽高...
        float textWidth = mBounds.width();
        float textHeight = mBounds.height();
        //第五步 用画布将文字写到具体的位置上!
        //View的总宽一半减去文字长度一半,高度的一般+字体高度的一半,其实就是字体在View中间显示!!!额...
        canvas.drawText(text , getWidth()/2 - textWidth/2, getHeight()/2 + textHeight/2 , mPaint);

    }
//这里是点击事件,目的是点一次View改变一次mCount的数字,到达20点击从0开始...
    @Override
    public void onClick(View v) {
        mConut++;
        if(mConut > 20)
         mConut = 0;
        //这里调用该方法后,根据源码分析会回调到onDraw()方法进行重重绘!!
        invalidate();//调用该方法后悔回调到onDraw方法执行操作
    }
}
  • 组合控件
//首先定义一个title.xml文件
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="#c9f1ec">

    <Button
        android:id="@+id/btn_back"
        android:layout_width="60dp"
        android:layout_height="40dp"
        android:layout_marginLeft="5dp"
        android:layout_centerVertical="true"
        android:text="返回"
        android:textSize="18sp"
        />
    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="这是标题"
        android:layout_centerInParent="true"
        android:textSize="20sp"
        android:textColor="#0e0e0e"
        />
</RelativeLayout>
 - 然后定义一个View 继承FrameLayout
public class MyViewZuHe extends FrameLayout {
    private Button mBack;
    private TextView mTitle;

    public MyViewZuHe(Context context, AttributeSet attrs) {
        super(context, attrs);
        //传入布局,this是MyViewZuHe.this 是ViewGroup的子类
        LayoutInflater.from(context).inflate(R.layout.title, this);
        //找到相应的控件
        mBack = (Button) findViewById(R.id.btn_back);
        mTitle = (TextView) findViewById(R.id.tv_title);
        //设置控件的点击事件
        mBack.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                ((Activity)getContext()).finish();//结束活动!
            }
        });
    }
    //设置title 和back的名字  及back的点击事件;注意是public!!!
    public void setTitleText(String text){
        mTitle.setText(text);
    }
    public void setBackText(String text){
        mBack.setText(text);
    }
    public void setBackListener(OnClickListener listener){
        mBack.setOnClickListener(listener);
    }
}
 - 然后再Activity的布局文件中添加该**组合控件**
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
    android:background="#0334e6">

    <com.demo.zhihudemo.test.MyViewZuHe
        android:id="@+id/zu_he_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

    </com.demo.zhihudemo.test.MyViewZuHe>

图片
这里写图片描述

  • 在Activity中调用!!

 //得到组合View的实例,调用里面的public方法!
        mZuHe = (MyViewZuHe) findViewById(R.id.zu_he_view);
        mZuHe.setTitleText("改变标题");
        mZuHe.setBackText("改变");
        mZuHe.setBackListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext ,"改变点击事件",Toast.LENGTH_LONG).show();
            }
        });

更改后的图片
这里写图片描述
点击事件,会显示“改变点击事件”

  • 继承控件…
    • 首先准备一个删除按钮的布局
<Button xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/delete_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/delete_button"
    >

</Button>
  • 然后继承ListView ,
    • 首先onTouch事件中,如果删除按钮显示了,则点击别的地方就移除删除按钮;如果没有显示就用GestureDetector来处理当前手势操作
public class MyListView extends ListView implements View.OnTouchListener ,GestureDetector.OnGestureListener
{
    private GestureDetector mGestureDetector;//这里是手势探测器
    private  OnDeleteListener  mListener;//自定义的一个删除监听
    private ViewGroup mItemLayout;//这里是得到ItemView
    private View mDeleteBtn;//删除按钮布局
    private int mItemSelected;//判断当前选择的位置
    private boolean isDeleteShowed;

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mGestureDetector = new GestureDetector(context, this);
        setOnTouchListener(this);
    }
//设置删除监听,调用onDelete方法,根据位置索引来对数组集合进行操作!!
    public  void setDeleteListener(OnDeleteListener listener){
        mListener = listener;
    }
    @Override
    public boolean onDown(MotionEvent e) {
        if(! isDeleteShowed ){
            //返回该ListView(屏幕)的item的  position
            mItemSelected = pointToPosition((int)e.getX() ,(int) e.getY());
        }
        return false;
    }
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
       //判断X轴方向的速度大于Y轴方向是速度,即向左右快速滑动。。。显示  删除按钮
        if(!isDeleteShowed && Math.abs(velocityX) > Math.abs(velocityY) ){
            mDeleteBtn = LayoutInflater.from(getContext()).inflate(R.layout.delete_btn , null);
            mDeleteBtn.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    //点击删除按钮后,移除该删除按钮,
                    mItemLayout.removeView(mDeleteBtn);
                    mDeleteBtn = null;
                    isDeleteShowed = false;
                    //这里通过回调,将位置传入,然后后面通过调用 位置 来进行删除操作,
                    // 没有这行代码也就没有位置索引,就不能进行删除操作了
                    mListener.onDelete(mItemSelected);
                }
            });
            //得到ItemView
            //getChildAt(int position)参数 为当前屏幕里面显示的item 从0开始 并不是adapter里面的位置
            // 如果当前屏幕显示的list列表从 5--8 的数据 则
            //getChildAt(0) 为在list中position为5的那个item
            mItemLayout = (ViewGroup) getChildAt(mItemSelected - getFirstVisiblePosition() );
            //下面是将 删除按钮 在ItemView的位置设置!
            RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT
            );
            params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);//右中位置
            params.addRule(RelativeLayout.CENTER_VERTICAL);
            mItemLayout.addView(mDeleteBtn , params);

            isDeleteShowed = true;
        }
        return false;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if(isDeleteShowed){
            //触摸不是删除按钮的地方,则移除显示在ItemView的该删除按钮
            mItemLayout.removeView( mDeleteBtn );
            mDeleteBtn = null;
            isDeleteShowed = false;
            return false;
        }else {
            return mGestureDetector.onTouchEvent(event);
        }
    }

    public  interface OnDeleteListener{
        public void onDelete(int index);
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {

    }
}
  • 后面就是创建Adapter ,布局只有一个TextView ,之后在Activit的布局中使用MyListView
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <com.demo.NotePad.test.MyListView
        android:id="@+id/my_list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </com.demo.NotePad.test.MyListView>
</RelativeLayout>
  • 在Activity中进行操作
public class MyListViewActivity extends Activity {
    private MyListView mListView ;
    private MylvAdapter mAdapter;
    private List<String> mStrings = new ArrayList<String>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.my_listview);
        initString();//添加数据
        mListView = (MyListView) findViewById(R.id.my_list_view);
//这里通过删除监听中的位置索引来进行 删除操作!        
        mListView.setDeleteListener(new MyListView.OnDeleteListener() {
            @Override
            public void onDelete(int index) {
                mStrings.remove(index);
                mAdapter.notifyDataSetChanged();
            }
        });

        mAdapter = new MylvAdapter(this, 0 , mStrings);
        mListView.setAdapter(mAdapter);

    }

    private void initString() {
        for (int i = 0; i <20 ; i++) {
            mStrings.add("子列表第" + i +"项");
        }
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值