自定义View——View的基础知识

一、 什么是View

(1)View 是 Android 中所有控件的基类
不管是简单的控件 TextView、Button,还是复杂的 RecyclerView、LinearLayout 等,它们的共同基类是 View。

(2)ViewGroup 也继承自 View
View 本身可以是单个控件,也可以是一组控件,Android 中的 UI 组件都由 View 和 ViewGroup 组成

二、 View的位置参数

View 的位置主要由它的四个顶点确定,再简单来说,根据左上角右下角这两个相对的顶点即可确定 View 的位置。top 是左上角的纵坐标、left 是左上角的横坐标、right 是右下角的横坐标、bottom 是右下角的纵坐标。

摘自-Carson_Ho-自定义 View 基础

上述4个属性和 View 宽/高的关系是:

  • width=right-left
  • height=bottom-top
Android 坐标系:摘自"工匠若水"

工匠若水-Android应用坐标系统全面详解

三、 获取 View 的位置参数的方式

(1)top、left、right、bottom
View 类的源码中上述四个属性分别存储在以下变量中:mLeftmTopmRightmBottom,分别提供了对应的get方法。这四个方法获取的是 View 的原始状态相对于父容器的坐标,对 View 进行平移不会改变上述方法返回值。

摘自-Android View 坐标系
  • left=getLeft();
protected int mLeft;
public final int getLeft() {
    return mLeft;
}
  • top=getTop();
  • right=getRight();
  • bottom=getBottom();

示例如下:

布局文件
public class TestActivity extends AppCompatActivity {
    private static final String TAG = "TestActivity";
    private View childView;
    private View parentView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        childView = findViewById(R.id.child_view);
        parentView = findViewById(R.id.parent_view_ll);

        initListenter();
    }

    private void initListenter() {
        parentView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
            @Override
            public void onGlobalLayout() {
                parentView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                parentView.getTop();
                Log.d(TAG, String.format("parent::--->top=%d;bottom=%d;left=%d;right=%d", parentView.getTop(), parentView.getBottom(), parentView.getLeft(), parentView.getRight()));
            }
        });

         childView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
            @Override
            public void onGlobalLayout() {
                childView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                childView.getTop();
                Log.d(TAG, String.format("child::--->top=%d;bottom=%d;left=%d;right=%d", childView.getTop(), childView.getBottom(), childView.getLeft(), childView.getRight()));
            }
        });
}

打印结果

2020-07-26 17:05:41.137 8126-8126/? D/TestActivity: parent::--->top=873;bottom=1173;left=390;right=690
2020-07-26 17:05:41.138 8126-8126/? D/TestActivity: child::--->top=100;bottom=200;left=100;right=200

说明top、bottom、left、right:都是以父控件坐标系为参考的坐标

(2)x、y、translationX、translationY
Android 3.0 开始,View 类中增加了额外的几个参数:x、y、translationX、translationY,并提供了相应的方法获取相应的值。

  • x、y

getX()getY()方法获取的是 View 左上角相对于父容器坐标系的坐标,当 View 未发生平移时,getX()=getLeft()getY()=getTop()

public float getX() {
    return mLeft + getTranslationX();
}
  • translationX、translationY

getTranslationX()getTranslationY()方法获取的是 View 左上角相对于父容器的平移量

public float getTranslationX() {
    return mRenderNode.getTranslationX();
}

x、y、translationX、translationY的换算关系是
x=left+translationXy=top+translationY

(3)MotionEvent 中获取用户点击点位置坐标
可以重写 View 类中的onTouchEvent()方法

@Override
public boolean onTouchEvent(MotionEvent event) {

    event.getX();
    event.getY();

    event.getRawX();
    event.getRawY();
}

在方法中可以调用MotionEventgetX()getRawX()方法获取位置信息,二者的区别如下。

  • getX()
    得到触摸点相对于其所在组件坐标系的坐标

  • getRawX()
    得到触摸点相对于所在屏幕坐标系的坐标

摘自-Android View 坐标系

也可以为 View 设置onTouchListener(),在其中获取 MotionEvent 的位置坐标 自定义View(一)、基础概念和知识点

mButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d(TAG, String.format("x=%f;y=%f;rawX=%f;rawY=%f", event.getX(), event.getY(), event.getRawX(), event.getRawY()));
                return false;
            }
        });

示例如下:


布局文件
public class TestActivity extends AppCompatActivity {
    private static final String TAG = "TestActivity";
    private View childView;
    private View parentView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        childView = findViewById(R.id.child_view);
        parentView = findViewById(R.id.parent_view_ll);


        initListenter();
    }

    private void initListenter() {
        childView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // 触摸child-view的左上角那一点
                Log.d(TAG, String.format("x=%f;y=%f;rawX=%f;rawY=%f", event.getX(), event.getY(), event.getRawX(), event.getRawY()));
                // getX()、getY():以View自身坐标系为参考的坐标
                // getRawX()、getRawY():以屏幕坐标系为参考的坐标
                return false;
            }
        });
    }
}

四、 Android 中颜色相关内容

(1)Android 中的颜色模式
英文表示通道类型,数字表示用多少个二进制位表示对应通道。

(2)定义颜色的方式

  • 在 java 文件中
int color=Color.GRAY;
int color=Color.argb(127,255,0,0);
int color=0xaaff0000;// 高精度带透明度
  • 在 xml 文件(资源文件)中
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#3F51B5</color>
</resources>

其中颜色的写法有四种:高精度带透明度#aaff0000、高精度不带透明度#ff0000、低精度带透明度#af00、低精度不带透明度#f00

(3)引用颜色的方式

  • 在 java 文件中

API≥23

int color=getColor(R.Color.Red);

API<23

int color=getResources().getColor(R.Color.Red);
  • 在 xml 文件中

layout 文件中引用/res/values/color.xml文件中内容

android:background="@color/colorPrimary"

layout 文件中创建并引用颜色

android:background="#ff0000"

style 文件中引用/res/values/color.xml文件中内容

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="colorPrimary">@color/red</item>
</style>

style 文件中创建并引用颜色

android:background="#ff0000"

五、 Android 中的 style 和 theme

自定义 View——Android 中的 style 和 theme

六、View 的构造函数

本节是对Android中自定义样式与View的构造函数中的第三个参数defStyle的意义的学习笔记

public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
}

Q:View 类有4个构造函数,每个参数的含义?

  • Context context

Q:继承 View 类自定义控件时需要实现哪几个构造函数?

七、 ViewGroup.LayoutParams

自定义控件知识储备-LayoutParams的那些事
自定义控件知识储备-View的绘制流程

1. 是什么

顾名思义,布局参数。布局文件 XML 中,以layout_开头的属性都和LayoutParams有关。

2. 作用

  • 子 View 通过布局参数告诉父控件自己想要被如何放置

  • LayoutParams 基类(ViewGroup.LayoutParams,ViewGroup 的静态内部类)仅描述了 View 想要的宽度和高度

  • 不同的 ViewGroup 继承类对应不同的 ViewGroup.LayoutParams 子类

如:LinearLayout,LinearLayout.LayoutParams;RelativeLayout,RelativeLayout.LayoutParams

3. 使用

(1)ViewGroup.MarginLayoutParams

(2)在 XML 布局文件里使用 LayoutParams 中的各种属性

LayoutParams 常用在 XML 布局文件里,使用容器控件的 LayoutParams 中的各种属性来给孩子们布局。这种方式直观方便,可直接在预览界面中看到效果,但同时界面也被写死了,无法动态改变

(3)在代码中为父容器动态地添加子 View

下面是几种常见的添加 View 的方式。通过调用父控件的addView()完成添加。

LinearLayout parent = (LinearLayout) findViewById(R.id.ll_content);

// 1,直接添加一个 TextView,不主动指定LayoutParams
TextView textView1 = new TextView(this);
textView1.setText("哈哈");
textView1.setTextColor(Color.RED);
parent.addView(textView1);

// 2,先手动为 TextView 指定 LayoutParams,再添加
TextView textView2 = new TextView(this);
textView2.setText("哈哈");
textView2.setTextColor(Color.RED);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(300, 300);
textView2.setLayoutParams(lp);
parent.addView(textView2);

// 3,向容器中添加子View时,指定LayoutParams
TextView textView3 = new TextView(this);
textView3.setText("哈哈");
textView3.setTextColor(Color.RED);
LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(300, 300);
parent.addView(textView3, lp2);

LinearLayoutRelativeLayout等容器的addView()方法都是继承自ViewGroup类,该类中提供了4个addView()方法的重载。

  • ① addView(View child)
public void addView(View child) {
    addView(child, -1);
}
  • ② addView(View child, int index)
public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    // 得到子View自身设置的LayoutParams
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        // 若未设置,则调用generateDefaultLayoutParams()得到默认的布局参数
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}
  • ③ addView(View child, int index, LayoutParams params)
public void addView(View child, int index, LayoutParams params) {
    if (DBG) {
        System.out.println(this + " addView");
    }

    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }

    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}
  • ④ addView(View child, int width, int height)
public void addView(View child, int width, int height) {
    final LayoutParams params = generateDefaultLayoutParams();
    params.width = width;
    params.height = height;
    addView(child, -1, params);
}

上述方法中调用的其他相关方法。

  • ① generateDefaultLayoutParams()

ViewGroup 类中的generateDefaultLayoutParams()。

protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

在 LinearLayout 中重写了generateDefaultLayoutParams()方法。

@Override
protected LayoutParams generateDefaultLayoutParams() {
    if (mOrientation == HORIZONTAL) {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    } else if (mOrientation == VERTICAL) {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }
    return null;
}

这个方法的实现告诉我们,很有可能因为我们的懒,导致布局效果和我们理想的不一样。因此第一种添加 View 的方式不推荐,在添加 View 的时候我们应该手动指定 LayoutParams,这样不仅明确,而且易修改。

(3)自定义 LayoutParams

每个容器控件几乎都会有自己的 LayoutParams 实现,像 LinearLayout、RelativeLayout等。所有我们自定义 ViewGroup 时,几乎都要自定义相应的 LayoutParams。

  • LinearLayout.LayoutParams

先看下线性布局中布局参数的定义。

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    /**
     * Indicates how much of the extra space in the LinearLayout will be
     * allocated to the view associated with these LayoutParams. Specify
     * 0 if the view should not be stretched. Otherwise the extra pixels
     * will be pro-rated among all views whose weight is greater than 0.
     */
    @ViewDebug.ExportedProperty(category = "layout")
    public float weight;

    public int gravity = -1;

    /**
     * {@inheritDoc}
     */
    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        TypedArray a =
            c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);

        weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
        gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);

        a.recycle();
    }

    /**
     * {@inheritDoc}
     */
    public LayoutParams(int width, int height) {
        super(width, height);
        weight = 0;
    }

    /**
     * Creates a new set of layout parameters with the specified width, height
     * and weight.
     *
     * @param width the width, either {@link #MATCH_PARENT},
     *        {@link #WRAP_CONTENT} or a fixed size in pixels
     * @param height the height, either {@link #MATCH_PARENT},
     *        {@link #WRAP_CONTENT} or a fixed size in pixels
     * @param weight the weight
     */
    public LayoutParams(int width, int height, float weight) {
        super(width, height);
        this.weight = weight;
    }

    /**
     * {@inheritDoc}
     */
    public LayoutParams(ViewGroup.LayoutParams p) {
        super(p);
    }

    /**
     * {@inheritDoc}
     */
    public LayoutParams(ViewGroup.MarginLayoutParams source) {
        super(source);
    }

    /**
     * Copy constructor. Clones the width, height, margin values, weight,
     * and gravity of the source.
     *
     * @param source The layout params to copy from.
     */
    public LayoutParams(LayoutParams source) {
        super(source);

        this.weight = source.weight;
        this.gravity = source.gravity;
    }

    @Override
    public String debug(String output) {
        return output + "LinearLayout.LayoutParams={width=" + sizeToString(width) +
            ", height=" + sizeToString(height) + " weight=" + weight + "}";
    }

    /** @hide */
    @Override
    protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
        super.encodeProperties(encoder);

        encoder.addProperty("layout:weight", weight);
        encoder.addProperty("layout:gravity", gravity);
    }
}
  • 自定义 LayoutParams

参考文献

自定义View基础 - 最易懂的自定义View原理系列(1)
View 构造函数
深入理解Android View的构造函数

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

推荐阅读更多精彩内容