自定义控件的步骤:
1:自定义属性的声明和获取
2:onMeasure 测量
3:onLayout布局(在自定义ViewGroup中使用)
4:onDraw 绘制
5:onTouchEvent
6:onInterceptTouchEvent(ViewGroup中想去拦截子View的事件)
自定义属性的声明和获取
1、分析需要的自定义属性
2、在res/values/attra.xml定义声明
3、在layout xml文件中使用
4、在View的构造方法中获取属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SlantedTextView">
<attr name="slantedTextSize" format="dimension"/>
<attr name="slantedBackgroundColor" format="color"/>
<attr name="slantedText" format="string"/>
<attr name="slantedTextColor" format="color"/>
<attr name="slantedLength" format="dimension"/>
<attr name="slantedMode" format="enum">
<enum name="left" value="0"></enum>
<enum name="right" value="1"></enum>
<enum name="left_bottom" value="2"></enum>
<enum name="right_bottom" value="3"></enum>
<enum name="left_triangle" value="4"></enum>
<enum name="right_triangle" value="5"></enum>
<enum name="left_bottom_triangle" value="6"></enum>
<enum name="right_bottom_triangle" value="7"></enum>
</attr>
</declare-styleable>
</resources>
<com.test.SlantedTextView
android:id="@+id/slv_right_bt"
android:layout_width="@dimen/text_height"
android:layout_height="@dimen/text_height"
app:slantedBackgroundColor="@color/slanted_fb"
app:slantedMode="right_bottom"
app:slantedLength="@dimen/text_slanted"
app:slantedTextSize="10sp"
app:slantedText="JS"
android:layout_alignBottom="@+id/img_right_bt"
android:layout_alignParentEnd="true"/>
public void init(AttributeSet attrs) {
TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.SlantedTextView);
mTextSize = array.getDimension(R.styleable.SlantedTextView_slantedTextSize, mTextSize);
mTextColor = array.getColor(R.styleable.SlantedTextView_slantedTextColor, mTextColor);
mSlantedLength = array.getDimension(R.styleable.SlantedTextView_slantedLength, mSlantedLength);
mSlantedBackgroundColor = array.getColor(R.styleable.SlantedTextView_slantedBackgroundColor, mSlantedBackgroundColor);
if (array.hasValue(R.styleable.SlantedTextView_slantedText)) {
mSlantedText = array.getString(R.styleable.SlantedTextView_slantedText);
}
if (array.hasValue(R.styleable.SlantedTextView_slantedMode)) {
mMode = array.getInt(R.styleable.SlantedTextView_slantedMode, 0);
}
array.recycle();
}
onMeasure 测量
MeasureSpec
MeasureSpec代表一个32位的int值,高2位是SpecMode,测量模式,低30位是SpecSize,指在某种测量模式下的规格大小。
测量的模式:EXACTLY(明确设置的一个值,比如100dp),AT_MOST(最多不超过,wrap_content时使用),UNSPECIFIED(没有限制,ListView和ScrollView中使用)
使用requestLayout()方法当view确定自身已经不再适合现有的区域时,该view本身调用这个方法要求parent view重新调用他的onMeasure onLayout来对重新设置自己位置。特别的当view的layoutparameter发生改变,并且它的值还没能应用到view上,这时候适合调用这个方法。
setMeasuredDimension()会设置View的宽和高,我们只需要看getDefaultSize()方法,返回的一般就是specSize。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
onLayout布局
1、父控件决定子View的位置
2、尽可能将onMeasure中的一些操作移动到此方法中
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
通过setFrame方法设定View的四个顶点位置,View在父容器中的位置也就确定了。接着会调用onLayout方法,用来确定父容器中子元素的位置。
onDraw 绘制
1、专门绘制内容区域
2、Canvas.drawXXX 绘制过程中主要是使用Canvas的相关API,要熟悉这些
3、invalidate(),postInvalidate() 重新绘制
onTouchEvent
1、主要判断MotionEvent的ACTION_DOWN、ACTION_MOVE、ACTION_UP操作,并做相对应的响应
2、如果是多点触控,就要使用ACTION_POINTER_DWON和ACTION_POINTER_UP,并设置一个ActivePointer(多点触控时实际上起作用的)
onInterceptTouchEvent
一般在ViewGroup中决定是否要拦截该手势,返回true表示拦截该手势,比如在ScrollView中是否拦截ACTION_MOVE,是否该滑动
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true; //返回true,拦截事件
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_POINTER_UP:
break;
}
return mIsBeingDragged;
}
自定义ProgressBar Demo 地址