一:简单自定义View示例
(1):在values目录下创建自定义属性的XML,比如attrs.xml;
注意:这个文件名没有什么限制,可以随便取名字;本文是在values目录下的attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleImageView">
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>

(2)在View的构造方法中解析自定义属性的值并做相应的处理。
本文,我们需要解析circle_color这个属性的值。首先加载自定义属性集合CircleImageView,接着解析CircleImageView属性集合中CircleImageView_circle_color属性。解析完自定义属性后,通过recycle方法来释放资源。
public CircleImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView);
mColor= array.getColor(R.styleable.CircleImageView_circle_color, Color.RED);
array.recycle();
init();
}
(3):在布局文件中使用自定义属性
//为了使用自定义属性,需要在布局文件中添加schemas声明
<com.labo.mvpsample.CircleImageView
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#fff"
android:padding="20dp"
app:circle_color="#FF2553EC"
/>
(4):完整CircleImageView代码
package com.labo.mvpsample;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
/**
* author labo
* date on 2017/8/13.
* desc
* 1.需要考虑到wrap_content模式以及padding
* 2.对外提供属性,方便设置view的ui
*/
public class CircleImageView extends View {
private int mColor= Color.RED;//默认颜色
private Paint mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
// 如果View是在Java代码里面new的,则调用第一个构造函数
public CircleImageView(Context context) {
super(context);
init();
}
// 如果View是在.xml里声明的,则调用第二个构造函数
public CircleImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public CircleImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView);
mColor= array.getColor(R.styleable.CircleImageView_circle_color, Color.RED);
array.recycle();
init();
}
private void init() {
//给画笔设置颜色
mPaint.setColor(mColor);
}
/**
*直接继承View的控件,如果不在onMeasrue中对wrap_content做特殊处理,那么当外界在布局中使用
*wrap_content时就无法达到预期效果,就相当于使用match_parent;
*当设置wrap_content时,我们只需要给View指定一个默认的宽/高,对于非wrap_content,我们使用系统的测量值即可
*wrap_content它的specMode是AT_MOST模式。
* setMeasuredDimension()这个方法设置View的宽/高
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpceSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(200,200);
}else if (widthSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(200,heightSpecSize);
}else if (heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpceSize,200);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
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);
}
}

二:简单自定义ViewGroup示例
二:简单自定义ViewGroup示例
package com.mvpdemo.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
/**
* author labo
* date on 2017/9/12.
* desc 本文主要是想通过案例来完成对自定义ViewGroup的理解,具体方法处理并没有做到严谨,望见谅;
*/
public class HorizontalView extends ViewGroup {
private int lastInterceptX;
private int lastInterceptY;
private int lastX;
private int lastY;
int currentIndex = 0;
int childWidth = 0;
private Scroller scroller;
private VelocityTracker velocityTracker;
public HorizontalView(Context context) {
super(context);
initView();
}
public HorizontalView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public HorizontalView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
/**
* 对scroller和velocityTracker进行初始化
*/
private void initView() {
scroller = new Scroller(getContext());
velocityTracker = VelocityTracker.obtain();
}
/**
* 在onMeasure中需要对wrap_content和Padding属性进行处理。
* 当设置为wrap_content的时候,因为没有一个固定的宽高,需要我们进行设置宽高;
* MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode指测量模式,SpecSize指在某种测量模式下的规格大小。SpecMode有三类,
* UNSPECIFIED:父容器不对View有任何限制,要多大给多大,一般用于系统内部,表示一种测量状态;
* EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式;
* AT_MOST:父容器指定了一个可用大小SpceSize,View的大小不能超过这个值。对应于LayoutParams中的wrap_content;
* 对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure就可以确定View的测量宽/高;
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
//如果没有子元素,则设置宽和高都为0,这里我们采用简化的写法,我们传递的主要是思想;
//正常情况下,我们应该根据LayoutParams中的宽高来做相应的处理。接着根据widthMode和heightMode设置宽高;
if (getChildCount() == 0) {
setMeasuredDimension(0, 0);
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//如果宽和高都是AT_MOST,则宽度设置为所有子元素宽度的和,高度设置为第一个子元素的高度
//正常应该是最大子元素的高度和所有子元素的宽度加margin和Padding;
View childOne = getChildAt(0);
int childWidth = childOne.getMeasuredWidth();
int childHeight = childOne.getMeasuredHeight();
setMeasuredDimension(childWidth * getChildCount(), childHeight);
} else if (widthMode == MeasureSpec.AT_MOST) {
//如果宽度是AT_MOST,则宽度为所有子元素宽度的和
int childWidth = getChildAt(0).getMeasuredWidth();
setMeasuredDimension(childWidth * getChildCount(), heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
//如果高度是AT_MOST,则高度为第一个子元素的高度
setMeasuredDimension(widthSize, getChildAt(0).getMeasuredHeight());
}
}
/**
* 通过onLayout来布局子元素
* 每一种布局方式,子元素的布局方式都是不同的;
* 遍历所有子元素,如果子元素不是GONE,则调用子元素的layout方法将其放置到合适的位置上。
* 这里宽高我们采用简化方式:宽度是默认累加的,没有考虑子元素的margin和padding。高度默认是第一个元素的高度;
*/
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
int childCount = getChildCount();
int left = 0;
View child;
for (int j = 0; j < childCount; j++) {
child = getChildAt(j);
if (child.getVisibility() != View.GONE) {
int width = child.getMeasuredWidth();
childWidth = width;
child.layout(left, 0, left + width, child.getMeasuredHeight());
left += width;
}
}
}
/**
* 处理滑动冲突
* 如果子元素为RecyclerView,子元素为竖直滑动,HorizontalView为水平滑动,那么我们就需要处理滑动冲突;
* 解决办法:当事件为滑动的时候,比较滑动的水平距离和竖直距离,如果水平大于竖直,则为水平滑动,那么我们就需要在HorizontalView拦截滑动事件,调用onInterceptTouchEvent方法。
* 有一个场景(再次触摸屏幕阻止页面滑动):如果我们向左滑动切换到下一个页面的时候,在手指释放以后,页面会弹性滑动到下一个页面。此时我们想要停止页面切换;
* 页面在滑动中,我们按下屏幕,也就是发生down事件,我们任务要停止滑动。如果Scroller还没有执行完毕,我们需要终端Scroller;
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
int currentX = (int) ev.getX();//获取事件到控件左边的距离,当前事件的位置
int currentY = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int distanceX = currentX - lastInterceptX;
int distanceY = currentY - lastInterceptY;
//如果水平滑动距离大于竖直滑动,那么拦截事件;
if (Math.abs(distanceX) > Math.abs(distanceY)) {
intercept = true;
} else {
intercept = false;
}
Log.e("===","111"+intercept);
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
lastInterceptX = currentX;
lastInterceptY = currentY;
//在ACTION_DOWN方法,返回了false,也就是没有拦截事件,那么就不会调用我们的onTouchEvent事件。所以我们在onTouchEvent事件中是无法获取到坐标的。
//所以我们需要在onInterceptTouchEvent对lastX、lastY进行赋值;
lastX = currentX;
lastY = currentY;
return intercept;
}
/**
* 我们通过onInterceptTouchEvent来判断是否拦截事件,如果返回true表示拦截事件,那么事件交由HorizontalView自身处理。
* 那么需要重写onTouchEvent,来处理事件;
* 在onTouchEvent我们需要处理滑动切换页面,这里需要Scroller
* 通常情况下,滑动超过一半页面的宽度的时候才切换页面这样用户体验是不好的。如果滑动速度很快的话,我们也可以认定为用户想要滑动到其他页面。
* 所以我们需要在onTouchEvent中的ACTION_UP对快速滑动进行处理。在这里我们需要用到VelocityTracker
* VelocityTracker:是用来测量滑动速度的。
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
//将事件加入到VelocityTracker类实例中
velocityTracker.addMovement(event);
int currentX = (int) event.getX();
int currentY = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = currentX - lastX;
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP:
int distance = getScrollX() - currentIndex * childWidth;
//判断滑动的距离是否大于宽度的一半,如果大于一半就进行切换页面
if (Math.abs(distance) > childWidth / 2) {
if (distance > 0) {
currentIndex++;
} else {
currentIndex--;
}
} else {
//当滑动距离没有超过一半的时候,如果是快速滑动我们也任务是切换页面
//获得水平方向的速度,1000表示1000ms,表示1000ms速度的最大值
velocityTracker.computeCurrentVelocity(1000);
float xVelocity = velocityTracker.getXVelocity();
if (Math.abs(xVelocity) > 150) {//如果速度大于150,我们认为是快速滑动。
//这里,如果我们手指向左滑动,速度是小于0的,我们想要进入右面的页面;
if (xVelocity > 0) {
currentIndex--;
} else {
currentIndex++;
}
}
}
//对currentIndex进行赋值判断
currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ? getChildCount() - 1 : currentIndex;
//调用smoothScrollTo方法进行弹性滑动
smoothScrollTo(currentIndex * childWidth, 0);
//我们需要重置速度计算器
velocityTracker.clear();
break;
}
return super.onTouchEvent(event);
}
/**
* scrollTo(x,y):表示移动到一个具体的坐标点
* scrollBy(dx,dy):表示移动的增量为dx,dy,scrollBy实际上调用的是scrollTo
* scrollTo、scrollBy移动的是View的内容,如果在ViewGroup中使用,则是移动的所有的子View;
* 用scrollTo、scrollBy这两个方法进行滑动的时候,这个滑动过程是瞬间完成的。用户体验不是很好
* 如果我们想是实现过度滑动的效果,我们需要使用Scroller,设置滑动的时间。
*/
private void smoothScrollTo(int destX, int dextY) {
scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), dextY - getScrollY(), 1000);
invalidate();//刷新界面
}
/**
* Scroller本身是不能实现View的滑动的,它需要与View的computeScroll方法结合才能实现弹性滑动的效果。
* 在MotionEvent.ACTION_UP事件触发时调用startScroll方法->马上调用invalidate / postInvalidate方法 -> 会请求View重绘,
* 导致View.draw方法被执行->会调用View.computeScroll方法,此方法是空实现,需要自己处理逻辑。
* 根据时间的流逝动态计算一小段时间里View滑动的距离,并得到当前View位置,
* 再通过scrollTo继续滑动。即把一次滑动拆分成无数次小距离滑动从而实现弹性滑动。
*
*/
@Override
public void computeScroll() {
super.computeScroll();
//先判断computeScrollOffset,若为true(表示滚动未结束),
// 则执行scrollTo方法,它会再次调用postInvalidate,如此反复执行,直到返回值为false。
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();//通过不断的重绘不断的调用computeScroll方法
}
}
}
三、自定义流式布局
class MyFlowLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var width = 0 //FlowLayout的宽度
var height = 0//FlowLayout的高度
var lineWidth = 0 //每一行的宽度
var lineHeight = 0 //每一行的高度
/**
* 步骤一:获取ViewGroup的测量模式和测量大小
*/
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val measureWidth = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val measureHeight = MeasureSpec.getSize(heightMeasureSpec)
/**
* 步骤二:计算所有子view的宽高
*/
//遍历所有子view
for (i in 0 until childCount) {
//1、获取子view
val childView = getChildAt(i)
//2、让子视图进行自我测量
measureChild(childView, widthMeasureSpec, heightMeasureSpec)
//3、获取子view的宽高
var childWidth = childView.measuredWidth
var childHeight = childView.measuredHeight
//4、获取子view的margin,计算当前子view的宽高
val childParams = childView.layoutParams
//这里我们要重写generateLayoutParams方法。
if (childParams is MarginLayoutParams) {
childWidth += childParams.leftMargin + childParams.rightMargin
childHeight += childParams.topMargin + childParams.bottomMargin
}
/**
* 5、我们做的是FlowLayout流式布局,要计算换行。
* a:(如果当前行的宽度+下一个子view的宽度)小于ViewGroup的宽度,
* 我们将当前子控件的宽度累加到lineWidth上。
* b:(如果当前行的宽度+下一个子view的宽度)大于ViewGroup的宽度,
* 1、我们就需要进行换行,把当前lineWidth和lineHeight累加到ViewGroup上。
* 2、重新初始化lineWidth和lineHeight,
* 由于换行,那当前控件就是下一行控件的第一个控件,
* 那么当前行的行高就是这个控件的高,当前行的行宽就是这个控件的宽度值了。
*/
if ((lineWidth + childWidth) > measureWidth) {
//这里需要换行,所以把当前行的宽高,叠加到ViewGroup的宽高上。
width = Math.max(lineWidth, childWidth)
height += lineHeight
//注意:这里给下一行设置宽、高
lineHeight = childHeight
lineWidth = childWidth
} else {//不需要换行
//比较行高和当前子view的高,把最大高度赋值给当前行高
lineHeight = Math.max(lineHeight, childHeight)
lineWidth += childWidth
}
/**
* 6、因为我们是在比较的时候,把每一行的宽高叠加到ViewGroup的宽高上的。
* 所以当最后一个子view的时候,也就是最后一行。我们要把最后一行的宽高叠加到ViewGroup的宽高上。
*/
if (i == childCount - 1) {
width = Math.max(width, lineWidth)
height += lineHeight
}
}
/**
* 7、最后通过setMeasuredDimension把ViewGroup的宽高设置到系统中
* 当测量模式是MeasureSpec.EXACTILY的时候,我们就不需要计算viewgroup的大小了。
*/
val currentWidth = if (widthMode == MeasureSpec.EXACTLY) {
measureWidth
} else {
width
}
val currentHeight = if (heightMode == MeasureSpec.EXACTLY) {
measureHeight
} else {
height
}
setMeasuredDimension(currentWidth, currentHeight)
}
/**
* 布局所有子控件
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val viewGroupWidth = measuredWidth
var top = 0//当前坐标的top坐标
var left = 0
var lineWidth = 0
var lineHeight = 0
//1、遍历所以子view,给每个子view布局
for (i in 0 until childCount) {
//2、获取当前子view
val childView = getChildAt(i)
//3、获取当前子view的宽高
val childParams = childView.layoutParams
var childWidth = childView.measuredWidth
var childHeight = childView.measuredHeight
//4、把当前子view的margin累加到子view的宽高上
if (childParams is MarginLayoutParams) {
childWidth += childParams.leftMargin + childParams.rightMargin
childHeight += childParams.topMargin + childParams.bottomMargin
}
//5、判断是否换行
if ((lineWidth + childWidth) > viewGroupWidth) {//换行
top = lineHeight
left = 0
lineHeight += childHeight
lineWidth = childWidth
} else {//不换行
lineHeight = Math.max(lineHeight, childHeight)
lineWidth += childWidth
}
//6、计算当前子view的left、right、top、bottom
var lc = left
var tc = top
if (childParams is MarginLayoutParams) {
lc += childParams.leftMargin
tc += childParams.topMargin
}
var rc = lc + childView.measuredWidth
var bc = tc + childView.measuredHeight
//7、给子view进行layout布局
childView.layout(lc, tc, rc, bc)
//8、给下一个子view设置left值
//注意:这里不能把子view的rc设置给left,因为还需要加上子view的marginRight。
left += childWidth
}
}
/**
* 获取到childView,通过 child.getLayoutParams()获取child对应的LayoutParams实例。
* 将其强转成MarginLayoutParams;然后获取对应的margin值,计算childWidth时添加上左边间距和右边间距。
*/
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
return MarginLayoutParams(p)
}
override fun generateDefaultLayoutParams(): LayoutParams {
return MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
}
四、橡皮擦
/**
* author : Naruto
* date : 2020-10-04
* desc : 橡皮擦
* version:
*/
class EraserView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var mBitPaint: Paint = Paint()
private lateinit var mBmpDST: Bitmap
private lateinit var mBmpSRC: Bitmap
private val mPath: Path = Path()
private var mPreX = 0f
private var mPreY = 0f
init {
setLayerType(View.LAYER_TYPE_HARDWARE, null)//禁用硬件加速
mBitPaint.color = Color.RED
mBitPaint.style = Paint.Style.STROKE
mBitPaint.strokeWidth = 45f
mBmpSRC = BitmapFactory.decodeResource(resources, R.mipmap.naruto)
mBmpDST = Bitmap.createBitmap(mBmpSRC.width, mBmpSRC.height, Bitmap.Config.ARGB_8888)
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val layerId =
canvas?.saveLayer(
0f, 0f,
width.toFloat(),
height.toFloat(), null
)
//先把手指轨迹画到目标bitmap上
val c = Canvas(mBmpDST)
c.drawPath(mPath, mBitPaint)
//把目标图像画到画布上
canvas?.drawBitmap(mBmpDST, 0f, 0f, mBitPaint)
//计算源图像区域
mBitPaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_OUT))
canvas?.drawBitmap(mBmpSRC, 0f, 0f, mBitPaint)
mBitPaint.xfermode = null
canvas?.restoreToCount(layerId!!)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
mPath.moveTo(event.x, event.y)
mPreX = event.x
mPreY = event.y
return true
}
MotionEvent.ACTION_MOVE -> {
val endX = (mPreX + event.x) / 2
val endY = (mPreY + event.y) / 2
mPath.quadTo(mPreX, mPreY, endX, endY)
mPreX = event.x
mPreY = event.y
}
}
postInvalidate()
return super.onTouchEvent(event)
}
}
五、QQ拖拽效果
/**
* author : Naruto
* desc :
* 1、根据手指所在位置画一个圆
* 2、用贝塞尔曲线,链接两个圆。
* 3、添加TextView,显示消息数量。
* 4、当用户点击的时候,设置textview的位置为点击时候的坐标。用户未点击的时候,显示在原来的位置。
* 5、判断是否画圆(当用户点击的时候,根据当前用户的手指位置,是否在原来的位置内)
*/
class RedPointView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private var mStartPoint: PointF //起始的圆心位置
private var mCurPoint: PointF//手指当前位置
private var mPaint: Paint = Paint()
private var mPath: Path
private val DEFAULT_RADIUS = 40f
private var mRadius = DEFAULT_RADIUS
private var mTouch = false
private var isAnimStart = false//表示动画效果 当true的时候,贝塞尔曲线和之前的圆应该消失。也就不绘画了
private var mTv: TextView
private var mImg: ImageView
init {
mPaint.color = Color.RED
mPaint.style = Paint.Style.FILL
mPath = Path()
mStartPoint = PointF(100f, 100f)
mCurPoint = PointF()
val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
mTv = TextView(getContext())
mTv.layoutParams = params
mTv.setPadding(10, 10, 10, 10)
mTv.setTextColor(Color.WHITE)
mTv.setTextSize(10f)
mTv.setBackgroundResource(R.drawable.tip_anim)
mTv.setText("99+")
addView(mTv)
mImg = ImageView(getContext())
mImg.layoutParams = params
mImg.setImageResource(R.drawable.loading_main)
mImg.visibility = View.GONE
addView(mImg)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
event ?: return false
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//判断当前点击区域是否在textview内部
val rect = Rect()
val location = IntArray(2)
mTv.getLocationOnScreen(location)
rect.left = location[0]
rect.top = location[1]
rect.right = mTv.width + location[0]
rect.bottom = mTv.height + location[1]
if (rect.contains(event.rawX.toInt(), event.rawY.toInt())) {
mTouch = true
}
}
MotionEvent.ACTION_UP -> {
mTouch = false
if (mRadius < 9) {
isAnimStart = true
mImg.setX(mCurPoint.x - mTv.getWidth() / 2)
mImg.setY(mCurPoint.y - mTv.getHeight() / 2)
mImg.setVisibility(View.VISIBLE)
val animationDrawable = mImg.getDrawable() as AnimationDrawable
animationDrawable.start()
postDelayed({
animationDrawable.stop()
mImg.visibility = View.GONE
}, 1000)
mTv.visibility = View.GONE
} else {
mRadius = DEFAULT_RADIUS
}
}
}
mCurPoint.set(event.x, event.y)
postInvalidate()
return true
}
/**
* 一、onDraw和dispatchDraw的区别
* onDraw()的意思是绘制视图自身
* dispatchDraw()是绘制子视图
* 无论是View还是ViewGroup对它们俩的调用顺序都是onDraw()->dispatchDraw()
* 但在ViewGroup中,当它有背景的时候就会调用onDraw()方法,否则就会跳过onDraw()直接调用dispatchDraw();
* 所以如果要在ViewGroup中绘图时,往往是重写dispatchDraw()方法
* 在View中,onDraw()和dispatchDraw()都会被调用的,所以我们无论把绘图代码放在onDraw()或者dispatchDraw()中都是可以得到效果的,
* 但是由于dispatchDraw()的含义是绘制子控件,所以原则来上讲,在绘制View控件时,我们是重新onDraw()函数
* 总结:在绘制View控件时,需要重写onDraw()函数,在绘制ViewGroup时,需要重写dispatchDraw()函数。
* 二、save()、saveLayer、restore
* restore:每当调用Restore()函数,就会把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布,并在这个画布上做画。
* saveLayer(图层)会创建一个全新透明的bitmap,大小与指定保存的区域一致,其后的绘图操作都放在这个bitmap上进行。在绘制结束后,会直接盖在上一层的Bitmap上显示。
*/
override fun dispatchDraw(canvas: Canvas?) {
canvas ?: return
if (!mTouch || isAnimStart) {
mTv.x = mStartPoint.x - mTv.width / 2
mTv.y = mStartPoint.y - mTv.height / 2
} else {
canvas.drawCircle(mStartPoint.x, mStartPoint.y, mRadius, mPaint)
calculatePath()
canvas.drawCircle(mCurPoint.x, mCurPoint.y, DEFAULT_RADIUS, mPaint)
canvas.drawPath(mPath, mPaint)
mTv.x = mCurPoint.x - mTv.width / 2
mTv.y = mCurPoint.y - mTv.height / 2
}
super.dispatchDraw(canvas)
}
private fun calculatePath() {
val x = mCurPoint.x
val y = mCurPoint.y
val startX = mStartPoint.x
val startY = mStartPoint.y
// 根据角度算出四边形的四个点
val dx = x - startX
val dy = y - startY
val a = Math.atan(dy / dx.toDouble())
val offsetX = mRadius * Math.sin(a)
val offsetY = mRadius * Math.cos(a)
val distance = Math.sqrt(
Math.pow(
y - startY.toDouble(),
2.0
) + Math.pow(
x - startX.toDouble(),
2.0
)
).toFloat()
mRadius = DEFAULT_RADIUS - distance / 15
if (mRadius < 9) {
mRadius = 8F
}
// 根据角度算出四边形的四个点
val x1 = startX + offsetX
val y1 = startY - offsetY
val x2 = x + offsetX
val y2 = y - offsetY
val x3 = x - offsetX
val y3 = y + offsetY
val x4 = startX - offsetX
val y4 = startY + offsetY
val anchorX = (startX + x) / 2
val anchorY = (startY + y) / 2
mPath.reset()
mPath.moveTo(x1.toFloat(), y1.toFloat())
mPath.quadTo(anchorX, anchorY, x2.toFloat(), y2.toFloat())
mPath.lineTo(x3.toFloat(), y3.toFloat())
mPath.quadTo(anchorX, anchorY, x4.toFloat(), y4.toFloat())
mPath.lineTo(x1.toFloat(), y1.toFloat())
}
fun resetView() {
mTv.visibility = View.VISIBLE
mTouch = false
isAnimStart = false
mRadius = DEFAULT_RADIUS
}
}