实现自定义控件

自定义控件的分类

[1]通过系统提供的原生控件进行组合 来达到自定义的需求
[2]定义一个类继承View(继承原生的类)
定义一个类继承ViewGroup (五大布局都继承自viewgroup)

下拉列表(原生控件进行组合)

Paste_Image.png

功能分析:
由edittext 按钮 popupwindow listview 通过这样几个控件进行组合达到需求

实现步骤
**1先画布局定义布局 **

<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="250dp"
        android:layout_height="wrap_content" />
    <!--去掉背景-->
    <!--android:background="@null"-->
    <ImageButton
        android:id="@+id/ib_btn"
        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 当点击按钮的时候弹出popupwindow

//弹出popupwindow 宽高 和Edittext宽高一样
protected void showPopUpWindow() {
    //[0]准备popupwindow 要展示的数据(listview)
    ListView contentView = initListView();
    //[1]构造popupwindow
    Popupwindow没必要new多次, 因此先判断一下popupwindow是否为空
    if (popupWindow == null) {
        popupWindow = new PopupWindow(contentView, et_number.getWidth() - 8, 250, true);
        popupwindow默认不让获取焦点(因此使用构造方法为4个参数的)
        //参1:要展示的view 参2: 宽  参3: 高 参4:Boolean—popupwindow是否可以获取焦点
//设置背景
        //点击popupwindow外面popupwindow不消失,想让popupwindow消失,因此给popupwindow设置背景
        popupWindow.setBackgroundDrawable(new ColorDrawable());

    }
    //[2]展示popupwindow  anchor:
    //参1popupwindow依赖哪个控件(展示在哪个控件下方)
    // 参2  偏移量x
    //参3  偏移量y
    //偏移量可以让尽量popupwindow与依赖的控件对齐的一致
    popupWindow.showAsDropDown(et_number, 4, -4);

}

再设置adapter--点击事件

绘制实战--重写ondraw方法中—CANVAS.DRAW画

    //将new paint实例放在构造方法中
    //在ondraw方法中避免申请paint对象,因为有可能在画的过程中申请多次,最好写在构造方法中
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //[1]创建画笔类
        mPaint = new Paint();
    }

    //[1]画线 两点确定一条线
    canvas.drawLine(0,0,100,100,paint);
    //参1 参2   起点坐标 
    //参3 参4 终点坐标
    //参5 画笔

    //[2]画圆  需要知道圆心和半径
    //画圆  知道圆心 和 半径  通过cx cy确定圆心  radius:半径
    canvas.drawCircle(100,100,30,mPaint);
    //参1 参2  确定圆心坐标(在view上画圆,因此找圆心只要是view宽高的一半即可)
    //参3 半径
    //参4 paint

    //修改画笔的(属性)
    //修改画笔颜色(画笔默认的颜色是黑色的)
    mPaint.setColor(Color.RED);
    //设置画笔样式  空心(默认实心)
    mPaint.setStyle(Style.STROKE);
    //去除锯齿 
    mPaint.setAntiAlias(true);

    // [3]画图片
    //将图片转换成bitmap
    // 2把haha.jpg转换成一个bitmap对象
    mBitmap=BitmapFactory.decodeResource(

    getResources(),R

    .drawable.haha);
    //画(bitmap)
    canvas.drawBitmap(mBitmap,10,10,mPaint);
    //参1 bitmap
    //参2 距view 左边的位置
    //参3 距view 顶部的位置
    //参4 paint

    //[4]画三角形(画路径)
    //定义三个点 画一个三角形
    int x1 = 100, y1 = 0;
    int x2 = 20, y2 = 180;
    int x3 = 180, y3 = 180;
    //拿到path路径对象--先移动到第一个点--然后挨个连接--有头有尾(其实就是构造出三角形的路径)
    mPath=new

    Path();

    mPath.moveTo(x1,y1); //先移动到x1 y1点
    mPath.lineTo(x3,y3); //连接
    mPath.lineTo(x2,y2);
    mPath.lineTo(x1,y1);
    //将构造的路径画出来(画三角形)
    //把三个点连起来即为三角形
    canvas.drawPath(mPath,mPaint);
    //参1 path 参2 paint


    // [5]画扇形  画扇形drawArc
    //rectF控制画扇形的范围
    canvas.drawArc(rectF,0,swapAngle,false,mPaint);
    //参1 矩形区域(通过这个矩形来限定画的扇形的大小)
    //参2 开始的一个角度,与水平向右的夹角(顺时针为正)
    //参3 扫过的的一个角度,与水平向右的夹角(顺时针为正)
    //参4 bollean(true 圆弧有边  false 圆弧没有边)
    //参5 paint
    //构造矩形
    rectF=new

    RectF(5,5,170,170);

    //[6]动态画圆
    //通过画扇形drawArc方法来画圆(调用一次方法只能画一段圆弧)
    //我们只需模拟一些数据,动态的改变参3的值即可
    //因此需要定义一个变量,接受传来的值
    //将传来的值传给canvas.drawArc方法的参3(这个方法写在ondraw中)
    //请求重新绘制

    //设置点击事件开始—模拟数据画圆
    // 点击按钮 动态画圆
    public void click(View v) {

        // 开启一个子线程
        new Thread() {
            public void run() {
                // 模拟数据
                for (int i = 1; i <= 100; i++) {
                    // 开始画
                    myView.startDraw(i);
                    // 睡眠50毫秒(因为画的太快,开不出效果,睡一会儿)(睡眠开子线程)
                    SystemClock.sleep(50);
                }

            }

            ;
        }.start();
    }

    //找到所要画圆的view,创建方法,传递数据, 并请求重新绘制
    public void startDraw(int x) {
        //定义一个变量(成员变量)进行传值
        mprogress = x;
        //请求重新绘制
        // 请求重新绘制 --->onDraw方法就会执行
        // invalidate();

        // 如果不是ui 线程 应该调用下面这个方法()--请求重新绘制 --->onDraw方法就会执行
        postInvalidate();

    }

    //重写ondraw方法,画圆弧,
    //往当前控件上画内容
    @Override
    protected void onDraw(Canvas canvas) {
        // rectF控制画扇形的范围
        数据传入参数3
        float swapAngle = mprogress / 100f * 360;

        canvas.drawArc(rectF, 0, swapAngle, false, mPaint);
        // 参1 矩形区域(通过这个矩形来限定画的扇形的大小)
        // 参2 开始的一个角度,与水平向右的夹角(顺时针为正)
        // 参3 扫过的的一个角度,与水平向右的夹角(顺时针为正)
        // 参4 bollean(true 圆弧有边 false 圆弧没有边)
        // 参5 paint
    }
    //构造矩形
    rectF=new RectF(5,5,170,170);

可滑动开关 –继承view(自定义控件)

需求分析 :实际是由2张图片 叠加到一起组成一个View
自己做自定义控件就两种方式,继承view还是继承viewgroup---如果需要包裹孩子就继承viewgroup
定义一个类继承view---添加两个参数的构造方法

public class ToogleView extends View {
    public ToogleView(Context context, AttributeSet attrs) {
        super(context, attrs);
  }
}

使用该view--在xml中声明

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

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

</RelativeLayout>

测量-根据自己的需求对当前控件进行测量—调用onmeasure方法

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 自己测量控件的宽和高 和当前背景图片一样宽 一样高
        setMeasuredDimension(toogleBgBitmap.getWidth(), toogleBgBitmap.getHeight());
    }

    ///把图片变成bitmap---为了获取背景图片的宽高
    // [1]找到背景图片和滑动块图片 变成bitmap
    toogleBgBitmap=BitmapFactory.decodeResource(getResources(),R.drawable.toogle_background);
    toogleSlideBitmap=BitmapFactory.decodeResource(getResources(),R.drawable.toogle_slidebg);

不用排版,父类已经排好版
控件上绘制内容—重写ondraw—拿掉super—画好之后控件就展示上来了

// 往当前控件上画内容 要重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
    //画开关背景--画图片的时候不用paint
    canvas.drawBitmap(toogleBgBitmap, 0, 0, null);
    //画滑动块--画图片的时候不用paint
    canvas.drawBitmap(toogleSlideBitmap, slideLeftPosition, 0, null);
}

让当前的view处理事件—画好开关后——需要重写onTouchEvent—有按下 移动 抬起 等事件—返回true(当前view消费事件)

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
        case MotionEvent.ACTION_DOWN://按下
            break;
        case MotionEvent.ACTION_MOVE://移动
            break;
        case MotionEvent.ACTION_UP: // 手指抬起
            break;

        }
        return true; // 代表让当前控件处理事件 消费事件
    }

按钮移动效果
获取按下坐标—获取移动后的坐标—算出移动距离—让滑块移动这么长的距离(移动着么长距离其实就是重新再新的坐标上绘制按钮)

//1获取手指按下的x坐标
downX=event.getX();
//1获取手指移动后坐标
float moveX = event.getX();
//2 算出移动距离
float distanceX = moveX - downX;
//指定滑块移动到的位置---赋值给绘制滑块的参数3()
slideLeftPosition+=distanceX;
//更改起始点
// [4]改变一下起始点
downX=moveX;
//请求重新绘制 onDraw方法就会执行
invalidate();

限定滑块的左右边界

//右边界最大值—背景宽-滑块的宽
slideLeftMax = toogleBgBitmap.getWidth() - toogleSlideBitmap.getWidth();
//对边界进行判断
if (slideLeftPosition <= 0) {
    slideLeftPosition = 0;
} else if (slideLeftPosition >= slideLeftMax) {
    slideLeftPosition = slideLeftMax;
}

当手指抬起时,滑倒一个位置,抬起后接着滑动到左边或者右边

//当手指抬起时,获取到手指在的 位置,判断滑块的中心店位置,过去

//1算出背景中心点位置
float tooglebgCenterPosition = toogleBgBitmap.getWidth() / 2;
//2 算出滑动块的中心点位置 = slideLeftPosition + 滑块背景的一半
float sliecenterPosition = slideLeftPosition
        + toogleSlideBitmap.getWidth() / 2;
//判断滑块与背景的相对位置
if (sliecenterPosition <= tooglebgCenterPosition) {
    // 开关处于关
    slideLeftPosition = 0;
} else {
    slideLeftPosition = slideLeftMax;
}

View状态发生改变时的回调事件--实现开关事件对应的回调方法*

//我们想要在开关开启或者关闭的时候,执行一些逻辑要暴露此方法
//回调其实就是多态的一个应用定义接口—暴回调方法
// 定义接口
public interface OnToogleViewListener {
    // 当开关的状态发生改变的时候调用
    void onToogleState(boolean state);
}

//开关状态发生变化时调用该方法
//当手指抬起且开关状态发生变化—调用该方法判断手指是否抬起器开关状态是否改变定义变量判断是否抬手
/**
 * 是否抬起手 默认false
 */
private boolean isHandup;
//当手指抬起的时候置为true
        isHandup = true;
//当我们判断为抬起之后将其设置为false
        isHandup = false;

//定义开关默认状态
/**
 * 代表开关的默认状态
 */
private boolean isOpen;

//当手指抬起获取滑动后的开关状态

//3回调我们定义接口方法
if (isHandup){
    isHandup = false;
    //4获取滑动后的一个状态
    boolean isOpenTemp = slideLeftPosition>0;
    //判断如果临时状态与默认状态不一致则说明开关状态发生变化了
    if (isOpen!=isOpenTemp && mListener!=null) {
        //说明开关的状态改变了 我们就要触发我们定义的回调方法
        mListener.onToogleState(isOpenTemp);
        //发生改变后将发生改变后的状态—赋值给默认状态
                isOpen = isOpenTemp;
    }
}


//当开关状态放生变化时,触发回调方法(返回相应的值)
mListener.onToogleState(isOpenTemp);

//发生改变后将发生改变后的状态—赋值给默认状态
        isOpen = isOpenTemp;

//给view设置监听—传入接口类型(子类)—给接口类型成员变量赋值
/**
 * 设置开关的监听器
 */
public void setOnToogleViewListener(OnToogleViewListener l) {
    this.mListener = l;
}

//使用view的监听事件-- ,当监听到改变后执行触发后的方法       toogleView.setOnToogleViewListener(new OnToogleViewListener() {
    //这个什么时候被触发呢?
    @Override
    public void onToogleState(boolean state) {
        if (state) {
            Toast.makeText(getApplicationContext(), "开", 1).show();
        }else {
            Toast.makeText(getApplicationContext(), "关", 1).show();
        }
    }
});

设置点击事件—也包含在状态改变中
当按下时,获取按下的时间
long startTime = System.currentTimeMillis();
当手指抬起时记住抬起的时间
long endTime = System.currentTimeMillis();
判断
时间间隔小于200ms为点击事件
手指抬起的位置大于滑块的右边界
Else手指抬起的位置大于滑块的左边界
Else(大于200ms为滑动事件)

    case MotionEvent.ACTION_UP: // 手指抬起

            int endTime = (int) (System.currentTimeMillis() - startTime);
            System.out.println("endTiem:" + endTime);
            if (endTime < 200 && event.getX() > toogleSlideBitmap.getWidth()) {
                // 说明是点击事件
                slideLeftPosition = slideLeftMax;
            } else if (endTime < 200) {
                slideLeftPosition = 0;
            } else {
                // 1算出背景中心点位置
                float tooglebgCenterPosition = toogleBgBitmap.getWidth() / 2;
                // 2 算出滑动块的中心点位置 = slideLeftPosition + 滑块背景的一半
                float sliecenterPosition = slideLeftPosition
                        + toogleSlideBitmap.getWidth() / 2;
                if (sliecenterPosition <= tooglebgCenterPosition) {
                    // 开关处于关
                    slideLeftPosition = 0;
                } else {
                    slideLeftPosition = slideLeftMax;
                }
            }

            isHandup = true;

            break;

        }

给view设置自定义属性
在res/values下创建attrs.xml文件

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="toogleView">
        <attr name="toogleState" format="boolean" />
    </declare-styleable>

</resources>

在layout布局中声明

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

    <com.kailing.toogleview.ToogleView
        android:id="@+id/toogleView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        kailing:toogleState="false" />

</RelativeLayout>

在构造方法中获取属性值

// [2]通過AttributeSet获取属性值
String namespace = "http://schemas.android.com/apk/res/com.kailing.toogleview";
//参1 根据命名空间取值,参2属性名,参3 缺省值(默认值)
boolean toogleState = attrs.getAttributeBooleanValue(namespace, "toogleState", false);
//使用—属性
// [3]调用setToogleState
setToogleState(toogleState);

//[7]view自定义属性
//1)在values下创建一个attrs.xml 文件
//2)xml里面的内容抄袭系统定义好的
//3)在布局中使用  自己定义一个命名空间
//4)在当前的view的构造方法里同AttributeSet获取我们定义的值

ViewGroup绘制流程

定义一个类继承viewgroup(会自动重写onLayout方法—就在这个方法里完成排版)

//定义一个类继承viewgroup(会自动重写onLayout方法—就在这个方法里完成排版)
public class MyViewGroup extends ViewGroup {
    //添加两个参数的构造方法
    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

布局文件中使用(在这里自view还是看不见的)

<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.kailing.viewgroup.MyViewGroup
        android:id="@+id/myViewGroup1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/hello_world" />
    </com.kailing.viewgroup.MyViewGroup>

</RelativeLayout>

自己定义的viewgroup没有对孩子进行测量,和排版所以group里面的孩子显示不出来
测量—onmeasure—测量孩子
当前重写onmeasure是对当前自己的groupview进行测量,让父类进行测量即可

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    //1获取viewGroup的孩子
    // 参1因为就一个孩子--写0即可,多个孩子for循环即可
    // for (int i = 0; i < getChildCount(); i++) {}
    View childAt = getChildAt(0);
    //2//对孩子进行测量—实际调用孩子的measure方法
    //(参数写0是个非法值,当系统发现写0会采用默认的测量对当前的孩子进行测量)(默认的测量:孩子在声明的时候写的值--对应的模式)
    childAt.measure(0, 0);// 这行代码执行完--测量完了
    //3 获取孩子的宽度 高度(获取测量之后 的宽度和高度)
    measuredWidth = childAt.getMeasuredWidth();
    measuredHeight = childAt.getMeasuredHeight();
    // getMeasuredWidth 获取到测量之后的值 
    // getWidth()当view排版后才可以获取
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

排版—完成对孩子进行排版

    // 要在这个方法里面完成对孩子进行排版
    // 参数
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
//找到孩子
        View childAt = getChildAt(0);
//对孩子进行排版
        childAt.layout(l, t, measuredWidth, measuredHeight);

    }

绘制—不需要重写ondraw方法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容