UI 01: 自定义TextView,

2024-06-04  本文已影响0人  xqiiitan

自定义View 简介

自定义View,可以认为继承自View,系统没有的效果。
自定义属性,不能使用系统原有的属性名。

BaseLine计算

2. onMeasure 获取宽高的模式

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

MeasureSpec.AT_MOST: 布局中指定了wrap_content
MeasureSpec.EXACTLY: 制定了确定的值 100dp、match_parent/fill_parent
MeasureSpec.UNSPECIFIED: 尽可能的大。ListView/ScrollView 在测量布局的时候会用 UNSPECIFIED.

ScrollView 嵌套ListView,ListView只显示一个item?
ScrollView 传过来是UNSPECIFIED,

// ListView
if(heightMode== MeasureSpec.UNSPECIFIED){
    heightSize = mListPadding.top+mListPadding.bottom+ childHeight+ getVerticalFadingEdgeLength()*2;
}
if(heightMode== MeasureSpec.AT_MOST) {
    heightSize = measureHeightOfChildren(widthMeasureSpec, 0,NO_POSITION, heightSize,-1);
}

// 解决办法: 让其进入if(heightMode== MeasureSpec.AT_MOST)。

// MYListView extends ListView
onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 解决现实不全的问题。构建新的32bit值。高度是最大值。
    heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2, MeasureSpec.AT_MOST);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    
}

widthMeasureSpec:包含2个信息:模式占前面2位;值占后面30位。

3.onDraw()

canvas.drawText();
canvas.drawCircle();
canvas.drawArc();

4.onTouch() 处理事件分发, 使用了责任链的设计模式。

返回true,才会不断调用 move,up等事件。
否则,只调用一次down事件,然后再也进不来。

// 处理跟用户的交互,手指触摸等等。事件分发事件拦截。
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: // 按下
            break;
        case MotionEvent.ACTION_MOVE: // 移动
            break;
        case MotionEvent.ACTION_UP: // 手指抬起
            break;
    }
    return super.onTouchEvent(event); // 交给父类处理
}

5.自定义属性 attrs.xml

自定义是属性,用来配置的。一般用于自定义属性。


自定义TextView 02

布局加载到Activity:

LayoutInflater解析布局,实例化View是通过反射构造函数来创建。
LayoutInflater 调用的是哪个构造函数?

2.1 指定宽高(三种测量模式)
基线计算: Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
top 表示基线到文字最上面的位置的距离,是一个负值。
bottom 是基线到最下面的距离,是一个正值。
如果想要给予一个位子竖直居中,name居中位置的坐标假设为centerY
baseLineY = centerY - (fm.bottom -fm.top)/2- fm.top;
这样就可以确定基线的坐标。

center = (bottom-top) /2;
baselineY = (bottom-top) /2 - (fm.bottom -fm.top)/2- fm.top
          = (bottom + top - fm.bottom - fm.top)/2;  

int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
int baseLineY = getHeight()/2 + dy; // 中心点centerY = getHeight()/2;

默认继承自 ViewGroup 默认不会调用onDraw()方法。模板设计模式。
但是设置背景了,会显示出来。

// 
if(!dirtyOpaque) onDraw(canvas); // ViewGroup 默认进不来。
dispatchDraw(canvas);
onDrawForeground(canvas);

dirtyOpaque 是 false 才行 其实就是由 privateFlags -> mPrivateFlags

final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
               (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);

mPrivateFlags 是在哪里赋值的:在View的构造函数中调用 computeOpaqueFlags()方法。

// setBackgroundDrawable() 会调用下面的方法。
protected void computeOpaqueFlags() {
      // Opaque if:
      //   - Has a background
      //   - Background is opaque
      //   - Doesn't have scrollbars or scrollbars overlay
      if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) {
          mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;
      } else {
          // 背景不为空
          mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;
      }        
      final int flags = mViewFlags;        
      if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) ||
             (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY ||
             (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) {
         mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS;       
      } else {
         mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS;
      }
}

ViewGroup 为什么出不来 ViewGroup()--> initViewGroup()

private void initViewGroup() {
   // ViewGroup doesn't draw by default
   if (!debugDraw()) {
        setFlags(WILL_NOT_DRAW, DRAW_MASK); // View的方法
   }
   // ...
}
// 导致 mPrivateFlags 会重新赋值

思路: 目的就是改变 mPrivateFlags的值

// View.java
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

自定义属性

 <!--name 自定义View的名字 TextView-->
    <declare-styleable name="TextView">
        <!-- name 属性名称
        format 格式: string 文字  color 颜色
                    dimension 宽高 字体大小  integer 数字
                    reference 资源(drawable)
         -->
        <attr name="darrenText" format="string"/>
        <attr name="darrenTextColor" format="color"/>
        <attr name="darrenTextSize" format="dimension"/>
        <attr name="darrenMaxLength" format="integer"/>
        <!-- background 自定义View都是继承自View , 背景是由View管理的-->
        <!--<attr name="darrenBackground" format="reference|color"/>-->
        <!-- 枚举 -->
        <attr name="darrenInputType">
            <enum name="number" value="1"/>
            <enum name="text" value="2"/>
            <enum name="password" value="3"/>
        </attr>
    </declare-styleable>

layout中调用,设置自定义属性

 <com.ex.mydemo2.TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#F00"
        android:padding="10dp"
        app:darrenText="湖南Tom girl Jery dog!"
        app:darrenTextColor="#000"
        app:darrenTextSize="30sp" />

layout中调用,设置自定义属性

// 自定义组件 TextView
public class TextView extends View {
    private String mText;
    private int mTextSize = 15;
    private int mTextColor = Color.BLACK;
    private Paint mPaint;

    // 代码中 new 的时候调用
    public TextView(Context context) {
        this(context, null);
    }

    // 在布局layout中使用
    public TextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }
    // 不会自动调用,API>21才能使用。如有默认style时,在第二个构造函数中调用。
    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    /**
     * 统一样式。
     * <style name="default">
     * <item name="layout_width"></item>
     * <item name="layout_height"></item>
     * <style/>
     * <com.darren.view_day1.TextView
     * style=@"style/default"
     * android:text="Welcome"/>
     */
    // 在layout中使用,使用了style。调用第三个构造方法。但是会有style
    // 不会自动调用,如有默认style时,在第二个构造函数中调用。
    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 获取自定属性
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TextView);
        mText = array.getString(R.styleable.TextView_darrenText);
        mTextColor = array.getColor(R.styleable.TextView_darrenTextColor, mTextColor);
        mTextSize = array.getDimensionPixelSize(R.styleable.TextView_darrenTextSize, sp2px(mTextSize));
        array.recycle();

        mPaint = new Paint();
        mPaint.setAntiAlias(true); // 抗锯齿
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        //  默认给一个透明的背景
        // setBackgroundColor(Color.TRANSPARENT);
        setWillNotDraw(false);
    }

    private int sp2px(int sp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
                getResources().getDisplayMetrics());
    }

    // 自定义的测量方法,指定控件的宽高
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // 1. 确定的值,不需要计算
        int width = MeasureSpec.getSize(widthMeasureSpec);
        // 2.wrap_content 需要计算, 宽度(与字体长度、字体大小有关),用画笔来测量
        if (widthMode == MeasureSpec.AT_MOST) {
            Rect bounds = new Rect();
            // 获取文本的rect
            mPaint.getTextBounds(mText, 0, mText.length(), bounds);
            width = bounds.width() + getPaddingLeft() + getPaddingRight(); // 宽度考虑padding值
        }

        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (heightMode == MeasureSpec.AT_MOST) {
            Rect bounds = new Rect();
            mPaint.getTextBounds(mText, 0, mText.length(), bounds);
            height = bounds.height() + getPaddingTop() + getPaddingBottom();
        }
        setMeasuredDimension(width, height); // 设置控件的宽高
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 画文字 text  x  y  paint
        // x 就是开始的位置   0
        // y 基线 baseLine   求?   getHeight()/2知道的   centerY

        //dy 代表的是:高度的一半到 baseLine的距离
        Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
        // top 是一个负值  bottom 是一个正值.  bottom的值代表是  bottom是baseLine到文字底部的距离(正值)
        // 必须要清楚的,可以自己打印就好:
        // dy = 中心线 到 基线的距离.
        int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
        int baseLine = getHeight()/2 + dy; // 中心点centerY = getHeight()/2;
        int x = getPaddingLeft();
        canvas.drawText(mText, x, baseLine, mPaint); // baseLine文字绘制的基线
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onTouchEvent(event);
    }
}
上一篇 下一篇

猜你喜欢

热点阅读