自定义控件:QQ气泡效果粘性控件的实现
2017-02-18 12:04
253 查看
学习目的
了解几何图形工具的用法掌握画不规则图形的方法
应用场景:未读提醒,效果图:
绘制一帧的效果
画一帧粘性控件的步骤分析画一个固定圆
画一个拖拽圆
画中间连接部分
将中间连接部分水平放置,四个角的坐标定为固定值,分别标记上点的编号,矩形中心的点为控件点,画曲线时用
自定义一个GooView 继承View
public class GooView extends View { private Paint paint; public GooView(Context context) { this(context,null); } public GooView(Context context, AttributeSet attrs) { this(context, attrs,0); } public GooView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); //初始化画笔 paint = new Paint(); paint.setAntiAlias(true); paint.setColor(Color.RED); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //画中间连接部分 Path path = new Path(); //跳到点1,默认为(0f,0f) path.moveTo(250f, 250f); //从点1->点2 画曲线 path.quadTo(150f, 300f, 50f, 250f); //从点2->点3 画直线 path.lineTo(50f, 350f); //从点3->点4 画曲线 path.quadTo(150f, 300f, 250f, 350f); canvas.drawPath(path, paint); //画拖拽圆 canvas.drawCircle(90f, 90f, 16f, paint); //画固定圆 canvas.drawCircle(150f, 150f, 12f, paint); } }
第20-30 行用Path 画中间曲线部分
第25 行quadTo(x1,y1,x2,y2)方法可以画当前所在点到x2,y2 间的一条曲线,x1,y1 是当前点与x2,y2 间的一个控件点,它的位置决定曲线弯曲的方向和弧度,将GooView 显示到MainActivity 中
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); setContentView(new GooView(this)); } }
贝塞尔曲线
上述代码调用path.quadTo()画曲线,这种曲线叫贝塞尔曲线,有一个起点和终点,还可以有2个或3个控制点,其中控制点是控制曲线的弯曲形状,控制点不同,曲线的弯曲形状就不同。二阶贝塞尔曲线,三阶贝塞尔曲线
替换变量
分别给拖拽圆,固定圆的圆心,半径,两个附着点命名,修改GooView 的onDraw()方法
protected void onDraw(Canvas canvas) { super.onDraw(canvas); //固定圆的两个附着点 PointF[] mStickPoints = new PointF[]{ new PointF(250f, 250f),new PointF(250f, 350f) }; //固定圆的两个附着点 PointF[] mDragPoints = new PointF[]{ new PointF(50f, 250f),new PointF(50f, 350f) }; //控制点 PointF mControlPoint = new PointF(150f, 300f); //画中间连接部分 Path path = new Path(); //跳到点1,默认为(0f,0f) path.moveTo(mStickPoints[0].x, mStickPoints[0].y); //从点1->点2 画曲线 path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y); //从点2->点3 画直线 path.lineTo( mDragPoints[1].x, mDragPoints[1].y); //从点3->点4 画曲线 path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y); canvas.drawPath(path, paint); //画拖拽圆 //拖拽圆圆心 PointF mDragCenter = new PointF(90f, 90f); //拖拽圆半径 float mDragRadius = 16f; canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, paint); //画固定圆 //固定圆圆心 PointF mStickCenter = new PointF(150f, 150f); //固定圆半径 float mStickRadius = 12f; canvas.drawCircle(mStickCenter.x, mStickCenter.y, mStickRadius, paint); }
第3-14 行替换附着点及控制点
第30-40 行替换拖拽圆及固定圆的圆心及半径
将替换后的变量转换成GooView 的成员变量
// 固定圆圆心 PointF mStickCenter = new PointF(150f, 150f); // 固定圆半径 float mStickRadius = 12f; // 拖拽圆圆心 PointF mDragCenter = new PointF(90f, 90f); // 拖拽圆半径 float mDragRadius = 16f; // 固定圆的两个附着点 PointF[] mStickPoints = new PointF[] { new PointF(250f, 250f), new PointF(250f, 350f) }; // 固定圆的两个附着点 PointF[] mDragPoints = new PointF[] { new PointF(50f, 250f), new PointF(50f, 350f) }; // 控制点 PointF mControlPoint = new PointF(150f, 300f); @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 画中间连接部分 Path path = new Path(); // 跳到点1,默认为(0f,0f) path.moveTo(mStickPoints[0].x, mStickPoints[0].y); // 从点1->点2 画曲线 path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y); // 从点2->点3 画直线 path.lineTo(mDragPoints[1].x, mDragPoints[1].y); // 从点3->点4 画曲线 path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y); canvas.drawPath(path, paint); // 画拖拽圆 canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, paint); // 画固定圆 canvas.drawCircle(mStickCenter.x, mStickCenter.y, mStickRadius, paint); }
计算变量
拖拽圆和固定圆的圆心和半径已知,角3 的正弦值为两圆心纵坐标之差比上横坐标之差,则角3 的角度可知,则角1 可知,AB,AC 的长度即可计算出来,mDragPoints[0]的坐标可以计算出来,同理其它三个附着点坐标也可知。mControlPoint 为两圆心连线的中点
几何图形工具
/** * 几何图形工具 */ public class GeometryUtil { /** * As meaning of method name. * 获得两点之间的距离 * @param p0 * @param p1 * @return */ public static float getDistanceBetween2Points(PointF p0, PointF p1) { float distance = (float) Math.sqrt(Math.pow(p0.y - p1.y, 2) + Math.pow(p0.x - p1.x, 2)); return distance; } /** * Get middle point between p1 and p2. * 获得两点连线的中点 * @param p1 * @param p2 * @return */ public static PointF getMiddlePoint(PointF p1, PointF p2) { return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f); } /** * Get point between p1 and p2 by percent. * 根据百分比获取两点之间的某个点坐标 * @param p1 * @param p2 * @param percent * @return */ public static PointF getPointByPercent(PointF p1, PointF p2, float percent) { return new PointF(evaluateValue(percent, p1.x , p2.x), evaluateValue(percent, p1.y , p2.y)); } /** * 根据分度值,计算从start 到end 中,fraction 位置的值。fraction 范围为0 -> 1 * @param fraction * @param start * @param end * @return */ public static float evaluateValue(float fraction, Number start, Number end){ return start.floatValue() + (end.floatValue() - start.floatValue()) * fraction; } /** * Get the point of intersection between circle and line. * 获取通过指定圆心,斜率为lineK 的直线与圆的交点。 * * @param pMiddle The circle center point. * @param radius The circle radius. * @param lineK The slope of line which cross the pMiddle. * @return */ public static PointF[] getIntersectionPoints(PointF pMiddle, float radius, DoublelineK) { PointF[] points = new PointF[2]; float radian, xOffset = 0, yOffset = 0; if(lineK != null){ radian= (float) Math.atan(lineK); xOffset = (float) (Math.sin(radian) * radius); yOffset = (float) (Math.cos(radian) * radius); }else { xOffset = radius; yOffset = 0; } points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset); points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset); return points; } }
利用几何图形工具类计算四个附着点坐标及控件点坐标
protected void onDraw(Canvas canvas) { super.onDraw(canvas); float yOffset = mStickCenter.y - mDragCenter.y; float xOffset = mStickCenter.x - mDragCenter.x; Double lineK = null; if(xOffset != 0){ //xOffset 分母不能为0 lineK = (double) (yOffset/xOffset); } //计算四个附着点 mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius, lineK); mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, mStickRadius, lineK); //一个控制点 mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter); // 画中间连接部分 Path path = new Path(); // 跳到点1,默认为(0f,0f) path.moveTo(mStickPoints[0].x, mStickPoints[0].y); // 从点1->点2 画曲线 path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y); // 从点2->点3 画直线 path.lineTo(mDragPoints[1].x, mDragPoints[1].y); // 从点3->点4 画曲线 path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y); canvas.drawPath(path, paint); // 画拖拽圆 canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, paint); // 画固定圆 canvas.drawCircle(mStickCenter.x, mStickCenter.y, mStickRadius, paint); }
第3-17 行计算四个附着点及控制点坐标
1.4 计算固定圆半径
GooView 重写onSizeChanged()方法,计算状态栏高度@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //获取状态栏的高度,传入一个显示在屏幕上的view 即可 statusBarHeight = Utils.getStatusBarHeight(this); }
Utils.java
public class Utils { public static Toast mToast; public static void showToast(Context mContext, String msg) { if (mToast == null) { mToast = Toast.makeText(mContext, "", Toast.LENGTH_SHORT); } mToast.setText(msg); mToast.show(); } /** * 获取状态栏高度 * * @param v * @return */ public static int getStatusBarHeight(View v) { if (v == null) { return 0; } Rect frame = new Rect(); v.getWindowVisibleDisplayFrame(frame); return frame.top; } }
修改onDraw()方法
protected void onDraw(Canvas canvas) { super.onDraw(canvas); float yOffset = mStickCenter.y - mDragCenter.y; float xOffset = mStickCenter.x - mDragCenter.x; Double lineK = null; if(xOffset != 0){ //xOffset 分母不能为0 lineK = (double) (yOffset/xOffset); } //计算四个附着点 mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius, lineK); mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, mStickRadius, lineK); //一个控制点 mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter); //移动画布 canvas.save(); canvas.translate(0, -statusBarHeight); // 画中间连接部分 Path path = new Path(); // 跳到点1,默认为(0f,0f) path.moveTo(mStickPoints[0].x, mStickPoints[0].y); // 从点1->点2 画曲线 path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y); // 从点2->点3 画直线 path.lineTo(mDragPoints[1].x, mDragPoints[1].y); // 从点3->点4 画曲线 path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y); canvas.drawPath(path, paint); // 画拖拽圆 canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, paint); // 画固定圆 canvas.drawCircle(mStickCenter.x, mStickCenter.y, mStickRadius, paint); canvas.restore(); }
第18-20 行把画布向上移动状态栏的高度,移动前需要保存一下当前状态,做完操作后需要恢复一下状态,由于在onTouchEvent()中用的是getRawX(),getRawY()获取的是相对屏幕的坐标,所以GooView画图操作时需要向上移到一个状态栏的高度才能刚好和手指重合拖拽圆跟随手指移动时,随着拖拽与固定圆的距离的变大,固定圆的半径越来越小
//允许的最大距离 float farestDistance = 80f; /** * 通过两圆圆心的距离,计算固定圆的半径 * @return */ private float computeStickRadius() { //通过几何图形工具类可以计算出两圆圆心的距离,distance 是可以大于80f; float distance = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter); //需要的是0.0f -> 1.0f 的值,所在大于80f 让distance 等于80f distance = Math.min(farestDistance, distance); float percent = distance/farestDistance; //需要固定圆心半径在12f -> 3f 间变化,可以利用类型估值器 return evaluate(percent, mStickRadius, mStickRadius*0.25f); } //FloatEvaluator.java 中拷贝 public Float evaluate(float fraction, Number startValue, Number endValue) { float startFloat = startValue.floatValue(); return startFloat + fraction * (endValue.floatValue() - startFloat); } protected void onDraw(Canvas canvas) { super.onDraw(canvas); //通过两圆圆心的距离,计算固定圆的半径 float tempStickRadius = computeStickRadius(); float yOffset = mStickCenter.y - mDragCenter.y; float xOffset = mStickCenter.x - mDragCenter.x; Double lineK = null; if(xOffset != 0){ lineK = (double) (yOffset/xOffset); } //计算四个附着点 mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius,lineK); mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, tempStickRadius,lineK); //一个控制点 mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter); //移动画布 canvas.save(); canvas.translate(0, -statusBarHeight); // 画中间连接部分 Path path = new Path(); // 跳到点1,默认为(0f,0f) path.moveTo(mStickPoints[0].x, mStickPoints[0].y); // 从点1->点2 画曲线 path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y); // 从点2->点3 画直线 path.lineTo(mDragPoints[1].x, mDragPoints[1].y); // 从点3->点4 画曲线 path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y); canvas.drawPath(path, paint); // 画拖拽圆 canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, paint); // 画固定圆 canvas.drawCircle(mStickCenter.x, mStickCenter.y, tempStickRadius, paint); canvas.restore(); }
第2 行定义最大的拖拽距离为80f
第7-24 行拖拽圆与固定圆的距离大于80f 时,取80f,通过两圆圆心的距离与80f 相对可以求出一个0.0f
到1.0f 的值,再通过估值器可以获得固定圆的半径在mStickRadius,mStickRadius*0.25f 间的变化值
第27-28 行通过两圆圆心的距离计算固定圆的半径tempStickRadius
第39,67 行将mStickRadius 替换成计算出来的半径tempStickRadius
事件处理
事件处理的分析
超出最大范围:拖拽圆与固定圆断开,松手后消失超出最大范围:又放回去,恢复
没有超出最大范围:松手,回弹动画,恢复
事件处理的实现
修改onTouchEvent()方法//是否已经消失 private boolean isDisappear = false; //是否超出范围 private boolean isOutOfRange = false; public boolean onTouchEvent(MotionEvent event) { float x; float y; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //重置变量 isDisappear = false; isOutOfRange = false; x = event.getRawX(); y = event.getRawY(); updateDragCenter(x, y); break; case MotionEvent.ACTION_MOVE: x = event.getRawX(); y = event.getRawY(); updateDragCenter(x, y); float d = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter); // 超出范围断开 if (d > farestDistance) { isOutOfRange = true; invalidate(); } break; case MotionEvent.ACTION_UP: if (isOutOfRange) { // 刚刚超出了范围 float dis = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter); if (dis > farestDistance) { // 超出范围,松手,断开,消失 isDisappear = true; invalidate(); } else { // 超出范围,断开,又放回去了,恢复 updateDragCenter(mStickCenter.x, mStickCenter.y); } } else { // 没有超出范围,松手,回弹,恢复 final PointF startP = new PointF(mDragCenter.x, mDragCenter.y); ValueAnimator animator = ValueAnimator.ofFloat(1.0f); animator.setDuration(500); // 插值器,回弹效果 animator.setInterpolator(new OvershootInterpolator(4)); animator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { // 生成0.0f ->1.0f 间的值 float percent = animation.getAnimatedFraction(); // 计算从开始点startP 到mStickCenter 间的所有值 PointF p = GeometryUtil.getPointByPercent(startP, mStickCenter, percent); updateDragCenter(p.x, p.y); } }); animator.start(); } break; default: break; } return true; }
第1-2 行创建两个布尔变量记录是否已经消失及是否超出范围
第11-12 行手指重新按下时,重置变量
第21-27 行拖拽过程中记录是否超出范围
第32-38 行超出范围,松手,消失,标记当前为消失状态
第39-41 行超出范围,又放回去了,需要恢复,直接更新拖拽圆圆心为固定圆心即可
第45-62 行没有超出范围,松手,需要回弹动画,恢复
修改onDraw()方法
protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 通过两圆圆心的距离,计算固定圆的半径 float tempStickRadius = computeStickRadius(); float yOffset = mStickCenter.y - mDragCenter.y; float xOffset = mStickCenter.x - mDragCenter.x; Double lineK = null; if (xOffset != 0) { lineK = (double) (yOffset / xOffset); } // 计算四个附着点 mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius, lineK); mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, tempStickRadius, lineK); // 一个控制点 mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter); // 移动画布 canvas.save(); canvas.translate(0, -statusBarHeight); // 画出最大范围(参考) // 只画边线 paint.setStyle(Style.STROKE); canvas.drawCircle(mStickCenter.x, mStickCenter.y, farestDistance, paint); // 填充 paint.setStyle(Style.FILL); if(!isDisappear){ //没有消失时,才绘制内容 if (!isOutOfRange) { //没有超出范围时,才画连接部分和固定圆 // 画中间连接部分 Path path = new Path(); // 跳到点1,默认为(0f,0f) path.moveTo(mStickPoints[0].x, mStickPoints[0].y); // 从点1->点2 画曲线 path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y); // 从点2->点3 画直线 path.lineTo(mDragPoints[1].x, mDragPoints[1].y); // 从点3->点4 画曲线 path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y); canvas.drawPath(path, paint); // 画固定圆 canvas.drawCircle(mStickCenter.x, mStickCenter.y, tempStickRadius, paint); } // 画拖拽圆 canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, paint); } canvas.restore(); }
第31-54 行没有消失时,才绘制内容,没有超出范围时,才绘制连接部分及固定圆
事件的监听回调
定义监听接口private OnUpdateListener onUpdateListener; public OnUpdateListener getOnUpdateListener() { return onUpdateListener; } public void setOnUpdateListener(OnUpdateListener onUpdateListener) { this.onUpdateListener = onUpdateListener; } public interface OnUpdateListener{ //消失时回调 public void onDisappear(); //恢复时回调,分为超出范围恢复及没有超出范围恢复 public void onReset(boolean isOutOfRange); }
修改onTouchEvent()方法
public boolean onTouchEvent(MotionEvent event) { float x; float y; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //重置变量 isDisappear = false; isOutOfRange = false; x = event.getRawX(); y = event.getRawY(); updateDragCenter(x, y); break; case MotionEvent.ACTION_MOVE: x = event.getRawX(); y = event.getRawY(); updateDragCenter(x, y); float d = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter); // 超出范围断开 if (d > farestDistance) { isOutOfRange = true; invalidate(); } break; case MotionEvent.ACTION_UP: if (isOutOfRange) { // 刚刚超出了范围 float dis = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter); if (dis > farestDistance) { // 超出范围,松手,断开,消失 isDisappear = true; invalidate(); if(onUpdateListener != null){ onUpdateListener.onDisappear(); } } else { // 超出范围,断开,又放回去了,恢复 updateDragCenter(mStickCenter.x, mStickCenter.y); if(onUpdateListener != null){ onUpdateListener.onReset(true); } } } else { // 没有超出范围,松手,回弹,恢复 final PointF startP = new PointF(mDragCenter.x, mDragCenter.y); ValueAnimator animator = ValueAnimator.ofFloat(1.0f); animator.setDuration(500); // 插值器,回弹效果 animator.setInterpolator(new OvershootInterpolator(4)); animator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { // 生成0.0f ->1.0f 间的值 float percent = animation.getAnimatedFraction(); // 计算从开始点startP 到mStickCenter 间的所有值 PointF p = GeometryUtil.getPointByPercent(startP, mStickCenter, percent); updateDragCenter(p.x, p.y); } }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); //需要在动画结束时调用 if(onUpdateListener != null){ onUpdateListener.onReset(false); } } }); animator.start(); } break; default: break; } return true; }
第35-37 行标记消失时,回调onDisappear()方法
第41-42 行恢复时回调onReset()方法,此时超出过范围,所以参数传入true
第64-73 行添加动画监听,在动画结束时回调onReset()方法,此时没有超出范围,所以参数传入false
修改MainActivity 测试监听回调
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); GooView view = new GooView(this); setContentView(view); view.setOnUpdateListener(new OnUpdateListener() { @Override public void onReset(boolean isOutOfRange) { Utils.showToast(getApplicationContext(), "onReset:"+isOutOfRange); } @Override public void onDisappear() { Utils.showToast(getApplicationContext(), "onDisappear"); } }); } }
RecyclerView的处理
如效果图看到的红色圆形控件为TextView并不是我们的GooViewitem布局文件
<?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="60dp"> <TextView android:text="这是标题" android:layout_width="0dp" android:layout_weight="1" android:textSize="30sp" android:layout_height="wrap_content" android:id="@+id/tv_title"/> <!-- 此处是一个TextView加入一个圆形背景,并不是GooView 原因:RecyclerView的条目显示区域仅仅有一块,而GooView的显示区域需要整个屏幕,如果直接将GooView放在条目中,拖动后会影响GooView的显示,故:使用TextView来显示,GooView后期动态加入 --> <TextView android:id="@+id/tv_unReadMsgCount" android:layout_width="30dp" android:layout_height="30dp" android:text="6" android:textColor="#fff" android:textSize="23sp" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:gravity="center" android:layout_marginRight="10dp" android:background="@drawable/tv_showmsg_shape"/> </LinearLayout>
圆形背景tv_showmsg_shape
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="#f00"/> </shape>
处理RecyclerView
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main1); List<Msg> msgList=new ArrayList<>(); for (int i = 0; i < 50; i++) { msgList.add(new Msg("标题"+i,i)); } RecyclerView rlv= (RecyclerView) findViewById(R.id.rlv); rlv.setLayoutManager(new LinearLayoutManager(this)); rlv.setAdapter(new MsgAdapter(msgList)); } //适配器处理 public class MsgAdapter extends Adapter<MsgAdapter.MyViewHolder> { private List<Msg> msgList; public MsgAdapter(List<Msg> msgList) { this.msgList = msgList; } @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.rlv_item, parent,false); return new MyViewHolder(view); } @Override public void onBindViewHolder(MyViewHolder holder, int position) { holder.tv_title.setText(msgList.get(position).title); //判断当未读消息数等于0,则隐藏对应的TextView控件 int unReadMsgCount = msgList.get(position).unReadMsgCount; if (unReadMsgCount == 0) { holder.tv_unReadMsgCount.setVisibility(View.INVISIBLE); } else { holder.tv_unReadMsgCount.setVisibility(View.VISIBLE); holder.tv_unReadMsgCount.setText(unReadMsgCount+""); } } @Override public int getItemCount() { return msgList.size(); } public static class MyViewHolder extends RecyclerView.ViewHolder { public TextView tv_title; public TextView tv_unReadMsgCount; public MyViewHolder(View itemView) { super(itemView); tv_title = (TextView) itemView.findViewById(R.id.tv_title); tv_unReadMsgCount = (TextView) itemView.findViewById(R.id.tv_unReadMsgCount); } } }
加入RecyclerView后的事件分发问题(事件分发机制)
加入GooView后的处理
实现效果的原理:当用户触摸到右侧的圆形背景TextView的时候,让TextView隐藏,利用WindowManager添加GooView当松开手后,将GooView移除,让TextView显示GooView的准备工作
让GooView能够显示文本,定义为GooView设置文本的方法
private String GooViewText=""; public void setGooViewText(String gooViewText) { GooViewText = gooViewText; }
在onDraw方法中绘制文本
@Override protected void onDraw(Canvas canvas) { if (!isDisappear){ if (!isOutOfRange){ ... //绘制文本 注意:要先绘制拖拽圆,再绘制文本,否则会被盖住 drawGooViewText(canvas); ... } //绘制拖拽圆 canvas.drawCircle(dragCenter.x,dragCenter.y,dragRadius,paint); ... } } private void drawGooViewText(Canvas canvas) { paint.setColor(Color.WHITE); //在android中任何看到的视图都是矩形的 //计算文本的宽高:原理是将文本外套上一个矩形,矩形的宽高就是文本的宽高 paint.getTextBounds(GooViewText, 0, GooViewText.length(), rect); int textWidth = rect.width(); int textHeight = rect.height(); //注意:一般控件是以左上角为基准点,文本是以左下角为基准点的,故:x为拖拽圆圆心x坐标-文本宽度/2 float x=dragCenter.x-textWidth*0.5f; // y为拖拽圆圆心y坐标+文本宽度/2 float y=dragCenter.y+textHeight*0.5f; canvas.drawText(GooViewText,x, y, paint); paint.setColor(Color.RED); }
添加为GooView初始化位置的方法
public GooView initGooViewPosition(float rawX, float rawY) { stableCenter.set(rawX, rawY); dragCenter.set(rawX, rawY); return this; }
处理适配器为GooView设置触摸监听
@Override protected void convert(BaseViewHolder helper, Msg item) { ... //为控件设置触摸监听 tv_un_read_msg_count.setOnTouchListener(listener); }
触摸监听的实现:添加GooView并设置位置和文本
public class OnShowGooViewTouchListener implements View.OnTouchListener { private Context context; private GooView gooView; private WindowManager windowManager; private WindowManager.LayoutParams params; //处理构造方法,传入上下文 public OnShowGooViewTouchListener(Context context) { this.context = context; //创建GooView gooView = new GooView(context); //创建WindowManager,可以用来在任何界面上添加一个额外的视图 windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); //为GooView设置布局参数 params = new WindowManager.LayoutParams(); //设置GooView的宽高为MATCH_PARENT params.height = WindowManager.LayoutParams.MATCH_PARENT; params.width = WindowManager.LayoutParams.MATCH_PARENT; //设置GooView为透明,使得GooView出现后,用户可以看到下面的界面 params.format = PixelFormat.TRANSLUCENT; } private View mView; //重写onTouch方法. //和onTouchEvent类似,如果onTouchEvent返回true,OnTouchListener返回true,则优先将事件交给MotionEvent @Override public boolean onTouch(View v, MotionEvent event) { //当按下去的对TextView进行相关处理 if (event.getAction() == MotionEvent.ACTION_DOWN) { msg = (Msg) v.getTag(); String text = ((TextView) v).getText().toString(); //隐藏TextView v.setVisibility(View.INVISIBLE); mView = v; //获取按下的x,y坐标 float rawX = event.getRawX(); float rawY = event.getRawY(); //设置gooView显示的位置和文本 gooView.initGooViewPosition(rawX, rawY); gooView.setGooViewText(text); //把GooView加载到windowManager上显示 windowManager.addView(gooView, params); } //表示想要处理事件 return true; } }
出现的bug1:触摸TextView然后向上移动,GooView不懂,RecyclerView动
事件被RecyclerView拦截了,需要请求RecyclerView不要拦截事件。事件分发涉及的基本概念:
onTouchEvent:触摸事件的处理
dispatchTouchEvent:传递触摸事件
onInterceptTouchEvent:拦截事件传递
requestDisallowedInterceptTouchEvent(boolean disallowIntercept):请求自己的父布局不要拦截事件
事件分发原理:解决触摸监听优于onTouchEvent获取事件的问题,处理:
//请求RecyclerView不要抢夺事件 v.getParent():获取TextView的父亲布局,为条目的根部局 //requestDisallowInterceptTouchEvent():请求被调用者的父布局不要拦截事件,即RecyclerView不要拦截事件 v.getParent().requestDisallowInterceptTouchEvent(true);
出现的bug2:移动后抬起手,GooView不消失
原因:利用WindowManger添加的视图比较特殊,不能直接移除,需要用windowManager.removeView(view);来移除
但,作为OnShowGooViewTouchListener并不知道什么时候该移除GooView,故:使用接口回调来处理这个问题
在GooView中定义接口:
//定义接口 public interface OnGooViewChangedListener { //消失的回调 public void disappear(); //重置的回调 public void reset(); } private OnGooViewChangedListener onGooViewChangedListener; public void setOnGooViewChangedListener( OnGooViewChangedListener onGooViewChangedListener) { this.onGooViewChangedListener = onGooViewChangedListener; }
当消失的时候调用消失的回调方法:
case MotionEvent.ACTION_UP: ... if (isOutOfRange) { if (distance > maxDistance) { ... //调用接口消失的方法 if (onGooViewChangedListener != null) { onGooViewChangedListener.disappear(); } } else { ... //调用接口重置的方法 if (onGooViewChangedListener != null) { onGooViewChangedListener.reset(); } } } else { ... valueAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { //调用接口重置的方法 if (onGooViewChangedListener != null) { onGooViewChangedListener.reset(); } } }); ... } invalidate(); break;
在OnShowGooViewTouchListener中设置和实现监听,并实现两个回调方法
public class OnShowGooViewTouchListener implements View.OnTouchListener, GooView.OnGooViewChangedListener { public OnShowGooViewTouchListener(Context context) { ... gooView.setOnGooViewChangedListener(this); } //当GooView消失的时候,从WindowManager中移除 @Override public void disappear() { if (gooView.getParent() != null) { if (msg != null) { msg.unReadMsgCount = 0; } windowManager.removeView(gooView); } } //当GooView重置的时候,将GooView从WindowManager中移除 @Override public void reset() { if (gooView.getParent() != null) { windowManager.removeView(gooView); } //显示TextView mView.setVisibility(View.VISIBLE); } }
相关文章推荐
- 使用css实现QQ聊天气泡效果
- 模拟QQ侧滑控件 实现三种界面切换效果(知识点:回调机制,解析网络json数据,fragment用法等)。
- Android 仿QQ未读消息拖拽删除粘性控件效果
- BezierDemo源码解析-实现qq消息气泡拖拽消失的效果
- 自定义控件三部曲之绘图篇(十五)——QQ红点拖动删除效果实现(基本原理篇)
- C#+ html 实现类似QQ聊天界面的气泡效果
- 安卓仿手机QQ消息BadgeView气泡跟随手指移动,并实现进出动画效果。
- QQ的粘性控件的实现原理
- html5 实现qq聊天的气泡效果
- 【HTML5】实现QQ聊天气泡效果
- android自定义控件SlidingButtonView实现类似QQ滑动删除效果
- 用border-image实现QQ气泡聊天窗效果
- Android自定义控件View(实现控件的动画效果,自定义类画简略时钟,TextView中文字逐一显示,动态设置progress的圆环/扇形ProgressBar等)
- 自定义控件三部曲之绘图篇(十五)——QQ红点拖动删除效果实现(基本原理篇)
- 【HTML5】简单实现QQ聊天气泡效果
- Qt+html+JavaScript实现类似QQ聊天界面的气泡效果
- WindowLess RichEdit 实现QQ聊天窗口的气泡效果,设计思路和方法。
- Java Swing实现的仿QQ气泡消息聊天窗口效果
- Ext.Net.button 和其他控件,实现不同点击事件效果(包括调用自定义控件)
- 实现QQ聊天气泡效果