Android自定义View最佳实践

1 自定义View的分类

1.1 继承View重写onDraw方法

主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,需要通过绘制的方式来实现,即重写onDraw()方法。采用这个方式需要自身支=warp_content,并且pading也要自己处理,比较考验你的功底了。

1.2 继承特定的View(比如TextView)(推荐)

一般是用于扩展某种已有的View功能,比如TextView,这种方法比较容易实现。这种方法不需要自己支持wrap_content和padding

1.3 继承特定的ViewGroup(比如RelativeLayout)(推荐)

当某种效果看起来像几种View组合的时候,可以采用这种方法来。不需要自己处理ViewGroup的测量和布局这两个过程。需要注意这种方法和方法2的区别,一般来说方法2能实现的效果方法4都能实现,两者主要的差别在于方法2更接近底层

1.4 继承ViewGroup派生出来的Layout

主要用于实现除了LinearLayout、RelativeLayout、FrameLayout外的新布局。当某些效果看起来像是几种View的组合的时候,可以采用这种方法。相对比较复杂,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。

2 自定义View的须知

2.1 让View支持wrap_content

因为①直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,当外界在布局中使用wrap_content时就相当于使用match_parent。(原因参考ndroid系统分析之View绘制流程与源码分析–3.1.3(3)

2.2 如果有必要,让你的View支持padding

因为①直接继承View的控件,如果不在draw方法中处理padding,那么padding属性无法起作用的。另外,②直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响不然将导致padding和子元素的margin失效。

2.3 尽量不要在View在中使用Handler,没必要

①View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你明确地要使用Handler来发送消息。

2.4 View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWinow

①如果线程或者动画需要停止时,onDetachedFromWindow是一个很好的时机②当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,此方法对应的是onAttachedToWindow③当包含此View的Activity启动时,View的onAttachedToWindow方法会被调用。同时,④当View变得不可见时,我们也需要停止线程和动画,如果不及时处理这种问题,可能会造成内存泄露。

2.5 View带有滑动嵌套情形时,需要处理好滑动冲突

详细参考:Android系统分析之事件分发机制详解

3 自定义View的实例

3.1 继承View重写onDraw方法

(1)在values目录下面创建自定义属性的xml,比如attrs.xml,也可以其他名字,名字没什么限制,不过为了规范,统一写在attrs.xml

<declare-styleable name="CircleView"> // 自定义属性集合
        <attr name="circle_color" format="color" />
        <attr name="circle_width" format="dimension" />
        <attr name="circle_height" format="dimension" />
</declare-styleable>

(2)第二步,在View的构造方法里解析到我们这个属性

private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        if (null != attrs && !isInEditMode()) {
            // 加载自定义属性集合CircleView
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
            // 解析属性集合CircleView中的circle_color属性
            mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
            mWidth = a.getDimensionPixelSize(R.styleable.CircleView_circle_width, 200);
            mHeight = a.getDimensionPixelSize(R.styleable.CircleView_circle_height, 200);
            // 释放资源
            a.recycle();
        }

        initView();
    }

(3)在布局文件中使用自定义属性

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   // 添加schemas声明 xmlns:app,app是自定义属性的前缀,可以修改
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="vertical">

    <com.seniorlibs.view.ui.CircleView
        android:id="@+id/circleView1"
        android:layout_width="wrap_content"
        android:layout_height="200dp"
        android:layout_margin="20dp"
        android:background="@color/orange"
        android:padding="10dp"
        app:circle_color="#d0d0d0"
        app:circle_height="200dp"
        app:circle_width="200dp" />
</LinearLayout>

(4)CirecleView

/**
 * 继承于View重写onDraw()方法
 */
public class CircleView extends View {

    private int mColor = Color.RED;
    private int mWidth;
    private int mHeight;
    private Paint mPaint;

    public CircleView(Context context) {
        super(context);
        init(context, null, 0);
    }

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0);
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    /**
     * 初始化
     *
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        if (null != attrs && !isInEditMode()) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
            mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
            mWidth = a.getDimensionPixelSize(R.styleable.CircleView_circle_width, 200);
            mHeight = a.getDimensionPixelSize(R.styleable.CircleView_circle_height, 200);
            a.recycle();
        }

        initView();
    }

    private void initView() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(mColor);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeight);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
    }
}

(5)效果
在这里插入图片描述

3.2 继承特定的ViewGroup(比如RelativeLayout)

(1)在values目录下面创建自定义属性的xml,比如attrs.xml,也可以其他名字,名字没什么限制,不过为了规范,统一写在attrs.xml

<declare-styleable name="LineLayout">
        <!-- string/integer/boolean:基本属性-->
        <attr name="titleText" format="string" />
        <!-- color:颜色-->
        <attr name="backgroundColor" format="color" />
        <attr name="textColor" format="color" />
        <!-- dimension:尺寸-->
        <attr name="textSize" format="dimension" />
        <!-- reference:引用-->
        <attr name="icon" format="reference" />

        <attr name="endText" format="string" />
        <attr name="srcWidth" format="dimension" />
        <attr name="srcHeight" format="dimension" />
        <attr name="srcPadding" format="dimension" />
        <attr name="border" format="boolean" />
        <attr name="hasIcon" format="boolean" />
    </declare-styleable>

(2)第二步,在View的构造方法里解析到我们这个属性

/**
     * 初始化自定义属性
     * 
     * @param context
     * @param attrs
     */
    private void initAttrs(Context context, AttributeSet attrs) {
        mBackgroundColor = ContextCompat.getColor(mContext, R.color.gray_f0f0f0);
        mSrcIcon = R.drawable.ic_launcher;
        mTextColor = ContextCompat.getColor(mContext, R.color.default_color);
        mTextSize = getResources().getDimensionPixelSize(R.dimen.small_text_size);
        mSrcWidth = LayoutParams.WRAP_CONTENT;
        mSrcHeight = LayoutParams.WRAP_CONTENT;
        mPadding = dp2px(mContext, 16);

        if (null != attrs && !isInEditMode()) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LineLayout);
            mTitleStr = a.getString(R.styleable.LineLayout_titleText);
            mBackgroundColor = a.getColor(R.styleable.LineLayout_backgroundColor, mBackgroundColor);
            mTextColor = a.getColor(R.styleable.LineLayout_textColor, mTextColor);
            mTextSize = a.getDimensionPixelSize(R.styleable.LineLayout_textSize, mTextSize);
            mEndStr = a.getString(R.styleable.LineLayout_endText);
            mSrcIcon = a.getResourceId(R.styleable.LineLayout_icon, mSrcIcon);
            mSrcPadding = a.getDimensionPixelSize(R.styleable.LineLayout_srcPadding, mSrcPadding);
            mSrcWidth = a.getDimensionPixelSize(R.styleable.LineLayout_srcWidth, mSrcWidth);
            mSrcHeight = a.getDimensionPixelSize(R.styleable.LineLayout_srcHeight, mSrcHeight);
            mHasBorder = a.getBoolean(R.styleable.LineLayout_border, false);
            mHasIcon = a.getBoolean(R.styleable.LineLayout_hasIcon, true);
            a.recycle();
        }
    }

(3)在布局文件中使用自定义属性

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   // 添加schemas声明 xmlns:app,app是自定义属性的前缀,可以修改
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="vertical">

    <com.seniorlibs.view.ui.LineLayout
        android:id="@+id/ll_clear_cache"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        app:backgroundColor="@color/gray_f0f0f0"
        app:endText="清理么"
        app:srcHeight="24dp"
        app:srcWidth="24dp"
        app:textColor="@color/default_color"
        app:titleText="清理缓存" />
</LinearLayout>

(4)资源

<!-- colors.xml -->
    <color name="white">#ffffff</color>
    <color name="gray_f0f0f0">#f0f0f0</color>
    <color name="default_color">#000000</color>
<resources>
    <!-- dimens.xml -->
    <dimen name="mini_text_size">10sp</dimen>
    <dimen name="micro_text_size">12sp</dimen>
    <dimen name="small_text_size">14sp</dimen>
    <dimen name="medium_text_size">16sp</dimen>
    <dimen name="large_text_size">18sp</dimen>
</resources>

(5)LineLayout

/**
 * Author: 陈李冠
 * Version: 1.0.0
 * Date: 2019/5/4
 * Mender:
 * Modify:
 * Description: 线栏布局-用于"发现/我的/设置/关于"
 */
public class LineLayout extends RelativeLayout {

    /**
     * 动态设置控件的id
     */
    private static final int ICON_ID = 100;
    private static final int TEXT_ID = 200;
    private static final int ARROW_ID = 300;
    private static final int END_TEXT_ID = 400;

    private Context mContext;
    private ImageView mIcon;
    private TextView mTvTitle;
    private TextView mTvEnd;
    private View mDivider;

    private int mBackgroundColor;
    private boolean mHasIcon = true;
    private int mTextColor;
    private int mTextSize;
    private String mTitleStr = "";
    private String mEndStr = "";
    private int mSrcIcon;
    private int mSrcPadding;
    private int mSrcWidth;
    private int mSrcHeight;
    private boolean mHasBorder = false;
    private int mPadding;

    public LineLayout(Context context) {
        super(context);
        init(context, null);
    }

    public LineLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public LineLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    /**
     * 初始化
     *
     * @param context
     * @param attrs
     */
    private void init(Context context, AttributeSet attrs) {
        mContext = context;

        initAttrs(context, attrs);

        // 通常:initViews()和addViews()二选一
        initViews();

        addViews();
    }

    /**
     * 初始化自定义属性
     *
     * @param context
     * @param attrs
     */
    private void initAttrs(Context context, AttributeSet attrs) {
        mBackgroundColor = ContextCompat.getColor(mContext, R.color.gray_f0f0f0);
        mSrcIcon = R.drawable.ic_launcher;
        mTextColor = ContextCompat.getColor(mContext, R.color.default_color);
        mTextSize = getResources().getDimensionPixelSize(R.dimen.small_text_size);
        mSrcWidth = LayoutParams.WRAP_CONTENT;
        mSrcHeight = LayoutParams.WRAP_CONTENT;
        mPadding = dp2px(mContext, 16);

        if (null != attrs && !isInEditMode()) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LineLayout);
            mTitleStr = a.getString(R.styleable.LineLayout_titleText);
            mBackgroundColor = a.getColor(R.styleable.LineLayout_backgroundColor, mBackgroundColor);
            mTextColor = a.getColor(R.styleable.LineLayout_textColor, mTextColor);
            mTextSize = a.getDimensionPixelSize(R.styleable.LineLayout_textSize, mTextSize);
            mEndStr = a.getString(R.styleable.LineLayout_endText);
            mSrcIcon = a.getResourceId(R.styleable.LineLayout_icon, mSrcIcon);
            mSrcPadding = a.getDimensionPixelSize(R.styleable.LineLayout_srcPadding, mSrcPadding);
            mSrcWidth = a.getDimensionPixelSize(R.styleable.LineLayout_srcWidth, mSrcWidth);
            mSrcHeight = a.getDimensionPixelSize(R.styleable.LineLayout_srcHeight, mSrcHeight);
            mHasBorder = a.getBoolean(R.styleable.LineLayout_border, false);
            mHasIcon = a.getBoolean(R.styleable.LineLayout_hasIcon, true);
            a.recycle();
        }
    }

    /**
     * 初始化布局View(如果需要通过布局方式获取View,可使用如下方法)
     */
    private void initViews() {
//        View root = LayoutInflater.from(mContext).inflate(R.layout.layout_simple_line, this, true);
//        TextView mTvContent = root.findViewById(R.id.tv_content);
    }

    /**
     * 动态添加View
     */
    private void addViews() {
        // 基础属性
        setBaseLayout();

        // icon
        addIcon();

        // text
        addTitleText();

        // arrow
        addArrow();

        // mEndStr
        addEndText();

        // divider
        addDivider();
    }

    /**
     * 设置基础属性
     */
    private void setBaseLayout() {
        setBackgroundColor(mBackgroundColor);
    }

    /**
     * 添加图标
     */
    private void addIcon() {
        LayoutParams iconLy = new LayoutParams(mSrcWidth, mSrcHeight);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            iconLy.addRule(ALIGN_PARENT_START);
        } else {
            iconLy.addRule(ALIGN_PARENT_LEFT);
        }
        iconLy.addRule(CENTER_VERTICAL);
        mIcon = new ImageView(mContext);
        mIcon.setLayoutParams(iconLy);
        mIcon.setPadding(mSrcPadding, mSrcPadding, mSrcPadding, mSrcPadding);
        mIcon.setImageResource(mSrcIcon);
        mIcon.setId(ICON_ID);
        if (!mHasIcon) {
            mIcon.setVisibility(View.GONE);
        }
        addView(mIcon);
    }

    /**
     * 添加标题文本
     */
    private void addTitleText() {
        LayoutParams textLy = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        if (mHasIcon) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                textLy.addRule(END_OF, ICON_ID);
            } else {
                textLy.addRule(RIGHT_OF, ICON_ID);
            }
            textLy.setMargins(mPadding, 0, 0, 0);
        }
        textLy.addRule(CENTER_VERTICAL);
        mTvTitle = new TextView(mContext);
        mTvTitle.setLayoutParams(textLy);
        mTvTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
        mTvTitle.setTextColor(mTextColor);
        mTvTitle.setText(mTitleStr);
        mTvTitle.setId(TEXT_ID);
        addView(mTvTitle);
    }

    /**
     * 添加行末文本
     */
    private void addEndText() {
        mTvEnd = new TextView(mContext);
        LayoutParams endTextLy = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
            endTextLy.addRule(LEFT_OF, ARROW_ID);
        } else {
            endTextLy.addRule(START_OF, ARROW_ID);
        }
        endTextLy.setMargins(0, 0, mPadding, 0);
        endTextLy.addRule(CENTER_VERTICAL);
        mTvEnd.setLayoutParams(endTextLy);
        mTvEnd.setText(mEndStr);
        mTvEnd.setTextColor(mTextColor);
        mTvEnd.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
        mTvEnd.setId(END_TEXT_ID);
        if (TextUtils.isEmpty(mEndStr)) {
            mTvEnd.setVisibility(GONE);
        }
        addView(mTvEnd);
    }

    /**
     * 添加箭头
     */
    private void addArrow() {
        LayoutParams arrowLy = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        arrowLy.addRule(CENTER_VERTICAL);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            arrowLy.addRule(ALIGN_PARENT_END);
        } else {
            arrowLy.addRule(ALIGN_PARENT_RIGHT);
        }
        ImageView arrow = new ImageView(mContext);
        arrow.setImageResource(R.drawable.ic_forward_black);
        arrow.setLayoutParams(arrowLy);
        arrow.setPadding(0, 0, mPadding, 0);
        arrow.setId(ARROW_ID);
        addView(arrow);
    }

    /**
     * 添加分割线
     */
    private void addDivider() {
        mDivider = new View(mContext);
        LayoutParams dividerLy = new LayoutParams(LayoutParams.MATCH_PARENT, dp2px(mContext,1));
        dividerLy.addRule(ALIGN_PARENT_TOP);
        mDivider.setLayoutParams(dividerLy);
        mDivider.setBackgroundColor(ContextCompat.getColor(mContext, R.color.default_color));
        if (!mHasBorder) {
            mDivider.setVisibility(View.GONE);
        }
        setPadding(mPadding, 0, 0, 0);
        addView(mDivider);
    }


    /**
     * 设置标题颜色/尺寸/文本
     *
     * @param color
     */
    public void setTextColor(int color) {
        mTvTitle.setTextColor(color);
    }

    /**
     * 设置标题尺寸(dp单位)
     *
     * @param size
     */
    public void setTextSize(float size) {
        mTvTitle.setTextSize(size);
    }

    public void setText(CharSequence text) {
        mTvTitle.setText(text);
    }

    public void setTypeface(int style) {
        mTvTitle.setTypeface(Typeface.defaultFromStyle(style));
    }

    /**
     * 设置Icon图标
     *
     * @param resId
     */
    public void setIcon(int resId) {
        mIcon.setImageResource(resId);
    }

    /**
     * 设置item项末文本
     *
     * @param charSequence
     */
    public void setEndText(CharSequence charSequence) {
        if (TextUtils.isEmpty(charSequence)) {
            mTvEnd.setVisibility(GONE);
        } else {
            mTvEnd.setVisibility(VISIBLE);
            mTvEnd.setText(charSequence);
        }
    }

    public TextView getEndText() {
        return mTvEnd;
    }

    /**
     * dp转px
     */
    public static int dp2px(Context context, int dp) {
        if (context == null) {
            return 0;
        }
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dp * scale + 0.5f);
    }

    /**
     * 转换sp为px
     */
    public static int sp2px(Context context, float spValue) {
        if (context == null) {
            return 0;
        }
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
}

(5)效果
在这里插入图片描述

3.3 继承特定的View(比如TextView)(推荐)

(1)问题产生
当我们使用TextView显示多行的文字时,为了美观,一般会加上行间距,这时就会用到lineSpacingExtra这个属性。Android 4.4以下,这个属性会影响到最后一行,最后一行也会有个行间距,而在5.0以上不会。
(2)问题分析
TextView有这个方法getLineBounds(int line, Rect bounds)可以得到指定行的y坐标,行的边框其实是包括行之间间隔的。行之间的空白间隔高度是行的最底部坐标减去文字的底部坐标。行的底部坐标为bounds.bottom.文字的底部坐标为baseline + decent。如下图:
在这里插入图片描述
外面蓝色的是边框,粉红色的是baseline,黑色的是文字的最底部坐标,这张图是在4.4上测试的,可以看到明显文字底部留还有有一大块空白
(3)问题解决

/**
 *  获取行距接口
 */
public interface IGetLineSpaceExtra {
	int getSpaceExtra();
}
/**
 * 计算并处理行距的TextView
 */
public class LineSpaceExtraTextView extends AppCompatTextView implements IGetLineSpaceExtra {

	private static final String TAG = LineSpaceExtraTextView.class.getSimpleName();
	private Rect mLastLineShowRect;
	private Rect mLastLineActualIndexRect;

	public LineSpaceExtraTextView(Context context) {
		this(context, null);
	}

	public LineSpaceExtraTextView(Context context, @Nullable AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public LineSpaceExtraTextView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		mLastLineShowRect = new Rect();
		mLastLineActualIndexRect = new Rect();
	}

	@Override
	public int getSpaceExtra() {
		return calculateExtraSpace();
	}

	/**
	 * 计算出最后一行多出的行间距的高
	 */
	public int calculateExtraSpace() {
		int result = 0;
		//界面显示的最后一行的index
		int lastLineShowIndex = 0;
		if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
			lastLineShowIndex = Math.min(getMaxLines(), getLineCount()) - 1;
		}
		//实际上的最后一行的index,当没设置maxLines时,跟lastLineShowIndex的值相等
		int lastLineActualIndex = getLineCount() - 1;

		if (lastLineShowIndex >= 0) {
			Layout layout = getLayout();
			int baseline = getLineBounds(lastLineShowIndex, mLastLineShowRect); // 指定行的基线的y坐标
			getLineBounds(lastLineActualIndex, mLastLineActualIndexRect);
			//只有“测量的高度”跟“getLayout的高度”相等时这种情况时最后一行才多出的行间距
			//因为有当设置maxLines时,通过“实际最后一行的底部坐标”-“显示最后一行的底部坐标”=“看不见那部分的高度”
			//然后判断“测量的高度”,跟“文字的总高度减去看不见的那部分高度”,相等才去算最后一行多出的行间距的高,不相等说明TextView没有底部空白间距
			if (getMeasuredHeight() == getLayout().getHeight() - (mLastLineActualIndexRect.bottom - mLastLineShowRect.bottom)) {
				result = mLastLineShowRect.bottom - (baseline + layout.getPaint().getFontMetricsInt().descent); // 文本上下的空隙
			}
		}
		Log.i(TAG, "extra space:" + result);
		return result;
	}
}
/**
 * 在5.0以下或者部分机型,如oppo R9sk,最后一行自动添加一个行间距的大小
 * 这个容器就是通过算出最后一行多出的行间距的高,然后用子view测量的总高度减去多余的行间距高作为该容器的高,子类多出部分不会显示出来
 */
public class LineSpaceExtraContainer extends RelativeLayout {

	public LineSpaceExtraContainer(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

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

		if (getChildCount() < 1) {
			throw new IllegalStateException("must has one child view");
		}

		View view = getChildAt(0);
		if (!(view instanceof IGetLineSpaceExtra)) {
			throw new IllegalStateException("child view mast is child of DividerLineTextView");
		}

		view.measure(widthMeasureSpec, heightMeasureSpec);
		//总高度减去多余的行间距高作为该容器的高
		setMeasuredDimension(view.getMeasuredWidth(), view.getMeasuredHeight() - ((IGetLineSpaceExtra) view).getSpaceExtra());
	}

	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		if (getChildCount() < 1) {
			throw new IllegalStateException("must has one child view");
		}

		//填充整个容器,忽略padding属性
		getChildAt(0).layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
	}
}
<com.....linespaceextraview.LineSpaceExtraContainer
        android:id="@+id/rl_book_description"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/layout_bookinfo"
        android:layout_marginLeft="@dimen/dp_16"
        android:layout_marginRight="@dimen/dp_16"
        android:layout_marginBottom="@dimen/dp_16"
        android:background="@drawable/clickable">

        <com....linespaceextraview.LineSpaceExtraTextView
            android:id="@+id/tv_book_description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:gravity="center"
            android:lineSpacingExtra="@dimen/dp_7"
            android:textColor="@color/gray_333333"
            android:textSize="@dimen/textsize_13" />
    </com.chuangyue.baselib.widget.linespaceextraview.LineSpaceExtraContainer>

(4)效果
在这里插入图片描述
(5)学习链接
填填Android lineSpacingExtra 的坑,解决行间距兼容性问题

3.4 继承ViewGroup派生出来的Layout

(1)HorizontalScrollViewEx

/**
 * 继承于ViewGroup派生特殊Layout-HorizontalScrollViewEx
 */
public class HorizontalScrollViewEx extends ViewGroup {
    private static final String TAG = "HorizontalScrollViewEx";

    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;

    // 分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    // 分别记录上次滑动的坐标(onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

    public HorizontalScrollViewEx(Context context) {
        super(context);
        init();
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        if (mScroller == null) {
            mScroller = new Scroller(getContext());
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            intercepted = false;
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                intercepted = true;
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
        }

        Log.d(TAG, "intercepted=" + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            scrollBy(-deltaX, 0);
            break;
        }
        case MotionEvent.ACTION_UP: {
            int scrollX = getScrollX();
            // 计算滚动的速度
            mVelocityTracker.computeCurrentVelocity(1000);
            float xVelocity = mVelocityTracker.getXVelocity();
            if (Math.abs(xVelocity) >= 50) {
                // 从左往右滑动,xVelocity为正值,页面向左滚;从右往左滑动,xVelocity为负值,页面向右滚
                mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
            } else {
                // 滑动慢时,根据滑动是否超过一半,判断是否需要滚动
                mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
                Log.e(TAG, "scrollX + mChildWidth + mChildWidth / 2 + mChildIndex : " + scrollX  + " " +  mChildWidth + " " + (mChildWidth / 2) + " " +  mChildIndex);
            }
            mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
            int dx = mChildIndex * mChildWidth - scrollX;
            smoothScrollBy(dx, 0);
            mVelocityTracker.clear();
            break;
        }
        default:
            break;
        }

        mLastX = x;
        mLastY = y;
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;

        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

(2)使用

public class ScrollViewExActivity extends Activity {
    private static final String TAG = "ScrollViewExActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.scroll_view_ex);
        Log.d(TAG, "onCreate");
        initView();
    }

    private void initView() {
        LayoutInflater inflater = getLayoutInflater();
        HorizontalScrollViewEx listContainer = findViewById(R.id.container);
        final int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
        final int screenHeight = MyUtils.getScreenMetrics(this).heightPixels;
        for (int i = 0; i < 3; i++) {
            // root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效。
            ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.content_layout, listContainer, false);
            layout.getLayoutParams().width = screenWidth;
            TextView textView = layout.findViewById(R.id.title);
            textView.setText("page " + (i + 1));
            layout.setBackgroundColor(Color.rgb(255 / (i + 1), 255 / (i + 1), 0));
            createList(layout);
            listContainer.addView(layout);
        }
    }

    private void createList(ViewGroup layout) {
        ListView listView = layout.findViewById(R.id.list);
        ArrayList<String> datas = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            datas.add("name " + i);
        }

        ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.content_list_item, R.id.name, datas);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Toast.makeText(ScrollViewExActivity.this, "click item", Toast.LENGTH_SHORT).show();
            }
        });
    }
}
<LinearLayout 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:background="#ffffff"
    android:orientation="vertical" >

    <com.seniorlibs.view.ui.HorizontalScrollViewEx
        android:id="@+id/container"
        android:layout_width="wrap_content"
        android:layout_height="match_parent" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:layout_marginBottom="5dp"
        android:text="TextView" />
    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#fff4f7f9"
        android:cacheColorHint="#00000000"
        android:divider="#dddbdb"
        android:dividerHeight="1.0px"
        android:listSelector="@android:color/transparent" />
</LinearLayout>

(3)效果

4 自定义View的思想

自定义算是一个综合体系,大多数情况下需要灵活分析从而找出最高效的方法。提取出一种思想,在面对陌生的自定义View时候,运用这个思想去快速解决问题:(1)首先掌握基本功,比如View的弹性滑动、滑动冲突、绘制原理等,这些都是自定义View所必须的,尤其是那些很酷炫的自定义View;(2)然后在面对新的自定义View时,要能够对其分类并选择合适的实现思路,自定义View的实现分类如上介绍;(3)另外平时需要多积累一些自定义View相关经验,并逐渐做到融会贯通,通过这种思想慢慢提高自定义View的水平。

发布了185 篇原创文章 · 获赞 207 · 访问量 59万+

猜你喜欢

转载自blog.csdn.net/chenliguan/article/details/89857718