Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

自定义控件01

$
0
0

1.AutoCompleteTextView

1 回顾Android中原生控件
1) autoCompleteTextview 这个控件展示数据和listview一样

2)listview展示数据的原理 : 需要一个适配器(arrayAdapter baseAdapter simpleadapter …)

3)实现代码 completionThreshold=”1”输入一个数就能搜索

3.1)在布局里面声明 
 <AutoCompleteTextView 
    android:id="@+id/actv" 
    android:layout_width="match_parent"
    android:completionThreshold="1"
    android:layout_height="wrap_content" />

3.2)创建适配器 展示数据

    setContentView(R.layout.activity_main);
    private static final String[] COUNTRIES = new String[] {
         "Belgium", "France", "Italy", "Germany", "Spain"
     };

    //[1]找到控件
    AutoCompleteTextView actv = (AutoCompleteTextView) findViewById(R.id.actv);
    //[2]actv这个控件展示数据 原理和listview展示数据的原理一样需要一个适配器 
    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
            android.R.layout.simple_dropdown_item_1line, COUNTRIES);
    //[3]设置适配器
    actv.setAdapter(adapter);

2.button和imagebutton

3.自定义控件的分类

[1]通过Android中原生的控件进行组合 来达到自定义的需求

[2]纯自定义控件  

    1)定义类继承View 

    2)定义类继承ViewGroup 

[3]View&ViewGroup    

     view是用户构建界面的基本模块 

     view在屏幕上占据的是矩形区域

     view的职责是绘制和事件的处理

     ViewGroup也继承View

     ViewGrtoup可以包含自己的孩子 

     平常我们使用的常见布局 比如 线性 相对 绝对 帧布局都直接继承ViewGroup 

4.下拉选择框

功能的分析 

 1)通过edittext 和 button  和popupWindow 和listview 的一个组合 达到需求 

 2)当点击按钮 弹出一个popupWindow  popupwindow里面展示的内容是由listview来填充

 3)当点击listview的条目 把该条目的内容取出来展示到edittext上 

 4)当点击listview 条目上的删除按钮可以把对应的条目的内容删除

实现步骤

[1]画Ui

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    tools:context=".MainActivity" >

    <EditText
        android:id="@+id/et_number"
        android:layout_width="200dp"
        android:layout_height="wrap_content" />

    <ImageButton
        android:id="@+id/ib_down_arrow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignRight="@id/et_number"
        android:background="@null"
        android:src="@drawable/down_arrow" />

</RelativeLayout>

[2]根据我们画的ui 写对应的逻辑 首先点击按钮 弹出popupwindow

    ListView listview =  initListView();

    /**
     * contentView popupwindw要展示的内容
     * width :popupwindo的宽 
     * true :代表popupwindow可以获取焦点
     */
    //[1]初始化popupwindow
    if (mpopupWindow == null) {
        mpopupWindow = new PopupWindow(listview, et_number.getWidth()-7, 250, true);
        //[1.1]点击popupwindow外部消失 
        mpopupWindow.setOutsideTouchable(true);
        mpopupWindow.setBackgroundDrawable(new ColorDrawable());


    }
    //[2]展示popuowindow  参数1 在哪个view下面  xoff:x轴偏移量  
    mpopupWindow.showAsDropDown(et_number, 4, -4);

[3]popupwindow实际展示的内容是listview的内容,初始化listview的内容

/**初始化listview的方法**/
private ListView initListView() {
    //[1]通过打气筒把一个布局转换成一个listview
    ListView listView = (ListView) View.inflate(getApplicationContext(), R.layout.listview, null);
    //[]设置listview的分割线 
    listView.setDivider(new ColorDrawable(Color.GRAY));
    listView.setDividerHeight(1);
    //[2]listview展示数据 创建适配器 
    listView.setAdapter(new MyAdapter());
    //[3]给listview条目设置点击事件 
    listView.setOnItemClickListener(new OnItemClickListener() {

        @Override
        public void onItemClick(AdapterView<?> parent, View view,
                int position, long id) {
            //[4]取出点击条目的内容  数据在哪里 存着就去哪里取 
            String data = lists.get(position);
            //[5]把data的数据展示到edittext上
            et_number.setText(data);
            //[6]让popupwindow消失 
            mpopupWindow.dismiss();         
        }
    });

    return listView;
}

[4]给listview条目设置点击事件,★ 当listview的条目上有button imageButton checkBox控件的时候会抢占listview条目点击事件的焦点,

 解决方案:

 1)我们不使用button等控件 换其他控件

 2)在listview条目的根上加如下属性 

1.android:descendantFocusability="blocksDescendants"

[5] 给listview条目上的删除按钮设置点击事件

ib_delete.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {

                //[5]把点中条目先从集合里面删除 
                lists.remove(position);
                //[6]通知适配器更新
                notifyDataSetChanged();

        }
        });

5.View绘制的流程

测量 ——–> 排版(布局) —–> 绘制

Measure ——->layout——->draw

★测量: 一般我们重写onMeasure方法

measure方法由于是final的所以不可以重写 ,我们发现有一个onMeasure方法 
那我就重写onMeasure方法,实际的测量工作是在onMeasure方法里面完成 

我们需要在onMeasure方法里面调用setMeasuredDimension 完成控件的测量

/**
 * widthMeasureSpec:父容器对myView宽度的期望  (layout_width)
 * heightMeasureSpec:父容器对myView高度的期望 (layout_height)
 * 参数1和2 实际上是由2部分组成  mode(模式) + size(具体控件大小) 组成 
 *   模式分三种
 *   [1]UNSPECIFIED 为指定 爹不管模式 父容器对孩子没有约束 
 *   [2]EXACTLY 精确的  父容器对孩子的大小有限制  布局的参数是一个具体的值 或者match_parent
 *   [3]AT_MOST 父容器对孩子的最大值有要求 对应布局中wrap_content
 *   
 *   我们在布局中声明的200dp宽和高 是如何传递到onMeasure方法里面的??
 *   [1]首先孩子像父容器申请200dp的宽layout_width和200dp的高layout_height ,父容器拿着孩子的申请打包成对孩子的期望widthMeasureSpec,heightMeasureSpec.
 *      调用孩子的measure方法  ---->会调用onMeasure方法 
 *   
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int mode = MeasureSpec.getMode(widthMeasureSpec);
    if (mode ==MeasureSpec.EXACTLY ) {
        //获取大小 
        int size = MeasureSpec.getSize(widthMeasureSpec);
        System.out.println("size:"+size);
    }
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

}

排版 layout

 l t r b 参数 如下图 

通过layout方法底层调用setFrame方法完成对view的上下左右进行赋值.

当我们在定义一个类继承View 的时候一般不需要重写此方法,因为view摆放的位置是由父容器发起的 

 draw方法实现对view绘制,我们在实际开发中一般会重写onDraw方法往当前的view上画内容 不会重写draw,先绘制背景,在绘制内容

 当绘制内容的时候需要重写onDraw();

6.绘制的实战

1)画线

canvas.drawLine(10, 10, 40, 40, paint);

2)画圆

// 使圆圆滑,没有锯齿
paint.setAntiAlias(true);
// 设置空心圆
paint.setStyle(Style.STROKE);
// 透明
paint.setAlpha(100);
canvas.drawCircle(100, 100, 20, paint);

3)画图片

canvas.drawBitmap(mBitmap, 0, 0, mPaint);

4)画三角形

//设置一个路径 画三角形
Path path = new Path();
int x1=10,y1=20;
int x2=100,y2=100;
int x3=180,y3=20;
//起点
path.moveTo(x1, y1);
//连接点
path.lineTo(x2, y2);
path.lineTo(x3, y3);
path.lineTo(x1, y1);
canvas.drawPath(path, paint);

5)画扇形

// 画扇形 第一个参数:指定一个矩形区域用来展示扇形,第2 和3 参数为角度 第四个参数:true表示显示扇形两边
// false表示隐藏扇形的两边
RectF oval = new RectF(10, 10, 170, 170);
canvas.drawArc(oval, 0, 360, true, paint);


canvas.drawArc(mrectF, 0, -90,true, mPaint);

 11-15 07:00:38.140: E/AndroidRuntime(4346): android.view.ViewRootImpl$CalledFromWrongThreadException: 
 Only the original thread that created a view hierarchy can touch its views. 只有主线程才可以更新ui

 调用postInvalidate();

6)动态画圆

在mainActivity 找到自定义控件,然后给控件添加方法调用
//在主线程更新UI调用此方法
invalidate();
//在子线程更新UI调用此方法
postInvalidate();

/** 动态画圆 **/
public void startCircle(final int i) {
    new Thread() {
        public void run() {
            for (int j = 0; j <= i; j++) {
                SystemClock.sleep(50);
                circle = j;
            }
            // 在子线程更新UI调用此方法
            postInvalidate();
        };
    }.start();

}

// 画扇形 第一个参数:指定一个矩形区域用来展示扇形,第2 和3 参数为角度 第四个参数:true表示显示扇形两边
// false表示隐藏扇形的两边
RectF oval = new RectF(10, 10, 200, 200);

float sweepAngle = circle / 100f * 360;
canvas.drawArc(oval, 0, sweepAngle, false, paint);

7.开关

需求分析:Android中 原生的开关非常丑陋 ,一般在企业中很少使用,需要我们自己定义view

实现代码

1)在布局中声明我们定义的myView

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >

<com.ithiema.toogleview.ToogleView
    android:id="@+id/toogleView1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true" />

</RelativeLayout>

2)在myView构造方法中获取开关背景图片(bitmap) 和 滑动块的背景图片

//[1]先获取开关的背景图片和滑动块的图片(bitmap) 
    mtoogleViewbg = BitmapFactory.decodeResource(getResources(), R.drawable.toogle_background);
    mSlideBg = BitmapFactory.decodeResource(getResources(), R.drawable.toogle_slidebg);
    //[2]算出滑块left最大边界值 
    slideLeftMaxSize = mtoogleViewbg.getWidth() - mSlideBg.getWidth();

3)在移动的方法里面算出移动的距离

float moveX = event.getX();
float distanceX = moveX - downX;
slideLeftPosition+=distanceX;
//[3]对边界进行判断
if (slideLeftPosition <=0) {
    slideLeftPosition = 0;
}else if (slideLeftPosition >=slideLeftMaxSize) {
    slideLeftPosition = slideLeftMaxSize;
}
downX = moveX;

4)手指抬起的时候出来的逻辑

//当手指抬起时候 滑动块是往左移动还是往右移动  
//算出滑动块的中心点位置 = slideLeftPosition + 滑动块宽度的一半   < 背景宽度的一半 就往左移动 否则往右移动 
if (slideLeftPosition + mSlideBg.getWidth() /2 < mtoogleViewbg.getWidth()/2 ) {
    slideLeftPosition = 0;
}else {
    slideLeftPosition = slideLeftMaxSize;
}

5)当手指抬起后 实现开关的功能

if (isHandup) {
    isHandup = false;
    //[4]获取开关当前的状态
    boolean isOpenTemp = slideLeftPosition > 0;
    if(isOpen != isOpenTemp && mToogleViewListener!=null){
        //[5]条件满足 触发回调方法
        mToogleViewListener.toogleState(isOpenTemp);
        isOpen = isOpenTemp;
    }
}

6)设置开关的状态

public void setToogleViewState(boolean b) {
    isHandup = true;
    if (b) {
        slideLeftPosition = slideLeftMaxSize; 
    }else {
        slideLeftPosition = 0; 
    }
}

代码:

private ToogleListener toogle;

public MyView(Context context, AttributeSet attrs) {
    super(context, attrs);

    // [1]先获取开关的背景图片和滑动块的图片(bitmap)
    toogle_background = BitmapFactory.decodeResource(getResources(),
            R.drawable.toogle_background);
    toogle_slidebg = BitmapFactory.decodeResource(getResources(),
            R.drawable.toogle_slidebg);
    tooglewidth = toogle_background.getWidth() / 2;
    maxX = toogle_background.getWidth() - toogle_slidebg.getWidth();
    halfX = maxX / 2;

    // 获取自定义的属性
    String name = "http://schemas.android.com/apk/res/com.heima.switchd";
    boolean attributeBoolean = attrs.getAttributeBooleanValue(name,
            "toogleState", false);
    setToogleViewState(attributeBoolean);

}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 这个view大小和背景图片宽高一样
    setMeasuredDimension(toogle_background.getWidth(),
            toogle_background.getHeight());
}

@Override
protected void onDraw(Canvas canvas) {

    canvas.drawBitmap(toogle_background, 0, 0, null);
    canvas.drawBitmap(toogle_slidebg, distend, 0, null);
    // 判断是抬起状态
    if (isUp) {
        isUp = false;
        // 判断当前的位置
        boolean isTemp = distend > 0;
        if (isOpen != isTemp && toogle != null) {
            // [5]条件满足 触发回调方法
            toogle.toogleState(isTemp);
            isOpen = isTemp;
        }
    }
}

// 处理消息
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        downX = event.getX();
        // 记录开始的时间
        startTime = System.currentTimeMillis();
        break;
    case MotionEvent.ACTION_MOVE:
        float moveX = event.getX();
        // 移动的距离
        distend += moveX - downX;
        if (distend <= 0) {
            distend = 0;
        } else if (distend >= maxX) {
            distend = maxX;
        }

        // 移动后再次赋值给初始值
        downX = moveX;

        break;
    case MotionEvent.ACTION_UP:
        isUp = true;
        // 记录抬起的时间
        long upTime = System.currentTimeMillis();
        float upX = event.getX();
        distend += upX - downX;
        long time = upTime - startTime;

        if (upX - downX < 5 && time < 200) {
            // 时间小于200 是点击事件
            if (upX > tooglewidth) {
                distend = maxX;
            } else {
                distend = 0;
            }
        } else {
            // 自动弹回
            if (distend <= halfX) {
                distend = 0;
            } else if (distend > halfX) {
                distend = maxX;
            }
        }

        break;

    default:
        break;
    }
    invalidate();// --->ondraw方法会执行
    return true;
}

/** 监听 **/
public void setOnToogleListener(ToogleListener toogleListener) {
    this.toogle = toogleListener;
}

/** 模拟写个接口 包括未实现的方法 **/
public interface ToogleListener {
    void toogleState(boolean result);
}

// 设置开关的状态
public void setToogleViewState(boolean b) {
    isUp = true;
    if (b) {
        distend = maxX;
        System.out.println("111");
    } else {
        distend = 0;
        System.out.println(2);
    }
}

7)自定义开关属性 首先在values下创建attrs文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ToogleView">
         <attr name="toogleState" format="boolean" />
    </declare-styleable>

</resources>


接下来自定义命名空间

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:itheima="http://schemas.android.com/apk/res/com.ithiema.toogleview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

   <com.ithiema.toogleview.ToogleView
        android:id="@+id/toogleView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        itheima:toogleState="true"
        android:layout_centerInParent="true" />

</RelativeLayout>

在自定义view 的构造方法中初始化

//[3]获取布局中定义的属性值 
String namespace = "http://schemas.android.com/apk/res/com.ithiema.toogleview";
boolean toogleState = attrs.getAttributeBooleanValue(namespace, "toogleState", false);

//[4]设置开关的状态
setToogleViewState(toogleState);

8 今天总结

[1]回顾Android原生控件  了解

[2]下拉列表    练习一下 

[3]view的绘制流程 ---测量---排版---绘制  掌握

[4]绘制的实战  掌握 

[5]开关 掌握

popupwindow 和 listView 组合 下拉选择框

View的绘制流程
1.测量 2.排版(布局) 3.绘制

measure layout draw

作者:yin13753884368 发表于2016/11/18 20:10:06 原文链接
阅读:26 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>