赞
踩
主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,需要通过绘制的方式来实现,即重写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变得不可见时,我们也需要停止线程和动画,如果不及时处理这种问题,可能会造成内存泄露。
(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)效果
(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)效果
(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 的坑,解决行间距兼容性问题
(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)效果
自定义算是一个综合体系,大多数情况下需要灵活分析从而找出最高效的方法。提取出一种思想,在面对陌生的自定义View时候,运用这个思想去快速解决问题:
(1)首先掌握基本功,比如View的弹性滑动、滑动冲突、绘制原理等,这些都是自定义View所必须的,尤其是那些很酷炫的自定义View;
(2)然后在面对新的自定义View时,要能够对其分类并选择合适的实现思路,自定义View的实现分类如上介绍;
(3)另外平时需要多积累一些自定义View相关经验,并逐渐做到融会贯通,通过这种思想慢慢提高自定义View的水平。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。