主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,需要通过绘制的方式来实现,即重写onDraw()方法。采用这个方式需要自身支=warp_content,并且pading也要自己处理,比较考验你的功底了。
一般是用于扩展某种已有的View功能,比如TextView,这种方法比较容易实现。这种方法不需要自己支持wrap_content和padding。
当某种效果看起来像几种View组合的时候,可以采用这种方法来。不需要自己处理ViewGroup的测量和布局这两个过程。需要注意这种方法和方法2的区别,一般来说方法2能实现的效果方法4都能实现,两者主要的差别在于方法2更接近底层。
主要用于实现除了LinearLayout、RelativeLayout、FrameLayout外的新布局。当某些效果看起来像是几种View的组合的时候,可以采用这种方法。相对比较复杂,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
因为①直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,当外界在布局中使用wrap_content时就相当于使用match_parent。(原因参考ndroid系统分析之View绘制流程与源码分析–3.1.3(3))
因为①直接继承View的控件,如果不在draw方法中处理padding,那么padding属性无法起作用的。另外,②直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响不然将导致padding和子元素的margin失效。
①View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你明确地要使用Handler来发送消息。
①如果线程或者动画需要停止时,onDetachedFromWindow是一个很好的时机。②当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,此方法对应的是onAttachedToWindow,③当包含此View的Activity启动时,View的onAttachedToWindow方法会被调用。同时,④当View变得不可见时,我们也需要停止线程和动画,如果不及时处理这种问题,可能会造成内存泄露。
详细参考:Android系统分析之事件分发机制详解
(1)在values目录下面创建自定义属性的xml,比如attrs.xml,也可以其他名字,名字没什么限制,不过为了规范,统一写在attrs.xml
// 自定义属性集合
(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)在布局文件中使用自定义属性
(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);
}
}
(1)在values目录下面创建自定义属性的xml,比如attrs.xml,也可以其他名字,名字没什么限制,不过为了规范,统一写在attrs.xml
(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)在布局文件中使用自定义属性
(4)资源
#ffffff
#f0f0f0
#000000
10sp
12sp
14sp
16sp
18sp
(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);
}
}
(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());
}
}
(4)效果
(5)学习链接
填填Android lineSpacingExtra 的坑,解决行间距兼容性问题
(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 datas = new ArrayList<>();
for (int i = 0; i < 50; i++) {
datas.add("name " + i);
}
ArrayAdapter 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();
}
});
}
}
(3)效果
自定义算是一个综合体系,大多数情况下需要灵活分析从而找出最高效的方法。提取出一种思想,在面对陌生的自定义View时候,运用这个思想去快速解决问题:(1)首先掌握基本功,比如View的弹性滑动、滑动冲突、绘制原理等,这些都是自定义View所必须的,尤其是那些很酷炫的自定义View;(2)然后在面对新的自定义View时,要能够对其分类并选择合适的实现思路,自定义View的实现分类如上介绍;(3)另外平时需要多积累一些自定义View相关经验,并逐渐做到融会贯通,通过这种思想慢慢提高自定义View的水平。