以下内容为原创,欢迎转载,转载请注明

来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/4182929.html

可拖拽的红点,(仿新版QQ,tab下面拖拽标记为已读的效果),拖拽一定的距离可以消失回调。

 

GitHub:DraggableFlagView(https://github.com/wangjiegulu/DraggableFlagView)

实现原理:

当根据touch事件的移动,不断调用onDraw()方法进行刷新绘制。

*注意:这里原来的小红点称为红点A;根据手指移动绘制的小红点称为红点B。

touch事件移动的时候需要处理的逻辑:

1. 红点A的半径根据滑动的距离会不断地变小。

2. 红点B会紧随手指的位置移动。

3. 在红点A和红点B之间需要用贝塞尔曲线绘制连接区域。

4. 如果红点A和红点B之间的间距达到了设置的最大的距离,则表示,这次的拖拽会有效,一旦放手红点就会消失。

5. 如果达到了第4中情况,则红点A和中间连接的贝塞尔曲线不会被绘制。

6. 如果红点A和红点B之间的距离没有达到设置的最大的距离,则放手后,红点B消失,红点A从原来变小的半径使用反弹动画变换到原来最初的状态

一些工具类需要依赖 AndroidBucket(https://github.com/wangjiegulu/AndroidBucket),nineoldandroid

使用方式:

<com.wangjie.draggableflagview.DraggableFlagView
       xmlns:dfv="http://schemas.android.com/apk/res/com.wangjie.draggableflagview"
android:id="@+id/main_dfv"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignParentBottom="true"
android:layout_margin="15dp"
dfv:color="#FF3B30"
/>
 public class MainActivity extends Activity implements DraggableFlagView.OnDraggableFlagViewListener, View.OnClickListener {

     @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
findViewById(R.id.main_btn).setOnClickListener(this); DraggableFlagView draggableFlagView = (DraggableFlagView) findViewById(R.id.main_dfv);
draggableFlagView.setOnDraggableFlagViewListener(this);
draggableFlagView.setText("7");
} @Override
public void onFlagDismiss(DraggableFlagView view) {
Toast.makeText(this, "onFlagDismiss", Toast.LENGTH_SHORT).show();
} @Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.main_btn:
Toast.makeText(this, "hello world", Toast.LENGTH_SHORT).show();
break;
}
}
}

DraggableFlagView代码:

 /**
* Author: wangjie
* Email: tiantian.china.2@gmail.com
* Date: 12/23/14.
*/
public class DraggableFlagView extends View {
private static final String TAG = DraggableFlagView.class.getSimpleName(); public static interface OnDraggableFlagViewListener {
/**
* 拖拽销毁圆点后的回调
*
* @param view
*/
void onFlagDismiss(DraggableFlagView view);
} private OnDraggableFlagViewListener onDraggableFlagViewListener; public void setOnDraggableFlagViewListener(OnDraggableFlagViewListener onDraggableFlagViewListener) {
this.onDraggableFlagViewListener = onDraggableFlagViewListener;
} public DraggableFlagView(Context context) {
super(context);
init(context);
} private int patientColor = Color.RED; public DraggableFlagView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DraggableFlagView);
int indexCount = a.getIndexCount();
for (int i = 0; i < indexCount; i++) {
int attrIndex = a.getIndex(i);
if (attrIndex == R.styleable.DraggableFlagView_color) {
patientColor = a.getColor(attrIndex, Color.RED);
}
}
a.recycle();
init(context);
} public DraggableFlagView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
} private Context context;
private int originRadius; // 初始的圆的半径
private int originWidth;
private int originHeight; private int maxMoveLength; // 最大的移动拉长距离
private boolean isArrivedMaxMoved; // 达到了最大的拉长距离(松手可以触发事件) private int curRadius; // 当前点的半径
private int touchedPointRadius; // touch的圆的半径
private Point startPoint = new Point();
private Point endPoint = new Point(); private Paint paint; // 绘制圆形图形
private Paint textPaint; // 绘制圆形图形
private Paint.FontMetrics textFontMetrics; private int[] location; private boolean isTouched; // 是否是触摸状态 private Triangle triangle = new Triangle(); private String text = ""; // 正常状态下显示的文字 private void init(Context context) {
this.context = context; setBackgroundColor(Color.TRANSPARENT); // 设置绘制flag的paint
paint = new Paint();
paint.setColor(patientColor);
paint.setAntiAlias(true); // 设置绘制文字的paint
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(ABTextUtil.sp2px(context, 12));
textPaint.setTextAlign(Paint.Align.CENTER);
textFontMetrics = paint.getFontMetrics(); } RelativeLayout.LayoutParams originLp; // 实际的layoutparams
RelativeLayout.LayoutParams newLp; // 触摸时候的LayoutParams private boolean isFirst = true; @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Logger.d(TAG, String.format("onSizeChanged, w: %s, h: %s, oldw: %s, oldh: %s", w, h, oldw, oldh));
if (isFirst && w > 0 && h > 0) {
isFirst = false; originWidth = w;
originHeight = h; originRadius = Math.min(originWidth, originHeight) / 2;
curRadius = originRadius;
touchedPointRadius = originRadius; maxMoveLength = ABAppUtil.getDeviceHeight(context) / 6; refreshStartPoint(); ViewGroup.LayoutParams lp = this.getLayoutParams();
if (RelativeLayout.LayoutParams.class.isAssignableFrom(lp.getClass())) {
originLp = (RelativeLayout.LayoutParams) lp;
}
newLp = new RelativeLayout.LayoutParams(lp.width, lp.height);
} } @Override
public void setLayoutParams(ViewGroup.LayoutParams params) {
super.setLayoutParams(params);
refreshStartPoint();
} /**
* 修改layoutParams后,需要重新设置startPoint
*/
private void refreshStartPoint() {
location = new int[2];
this.getLocationInWindow(location);
// Logger.d(TAG, "location on screen: " + Arrays.toString(location));
// startPoint.set(location[0], location[1] + h);
try {
location[1] = location[1] - ABAppUtil.getTopBarHeight((Activity) context);
} catch (Exception ex) {
} startPoint.set(location[0], location[1] + getMeasuredHeight());
// Logger.d(TAG, "startPoint: " + startPoint);
} Path path = new Path(); @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.TRANSPARENT); int startCircleX = 0, startCircleY = 0;
if (isTouched) { // 触摸状态 startCircleX = startPoint.x + curRadius;
startCircleY = startPoint.y - curRadius;
// 绘制原来的圆形(触摸移动的时候半径会不断变化)
canvas.drawCircle(startCircleX, startCircleY, curRadius, paint);
// 绘制手指跟踪的圆形
int endCircleX = endPoint.x;
int endCircleY = endPoint.y;
canvas.drawCircle(endCircleX, endCircleY, originRadius, paint); if (!isArrivedMaxMoved) { // 没有达到拉伸最大值
path.reset();
double sin = triangle.deltaY / triangle.hypotenuse;
double cos = triangle.deltaX / triangle.hypotenuse; // A点
path.moveTo(
(float) (startCircleX - curRadius * sin),
(float) (startCircleY - curRadius * cos)
);
// B点
path.lineTo(
(float) (startCircleX + curRadius * sin),
(float) (startCircleY + curRadius * cos)
);
// C点
path.quadTo(
(startCircleX + endCircleX) / 2, (startCircleY + endCircleY) / 2,
(float) (endCircleX + originRadius * sin), (float) (endCircleY + originRadius * cos)
);
// D点
path.lineTo(
(float) (endCircleX - originRadius * sin),
(float) (endCircleY - originRadius * cos)
);
// A点
path.quadTo(
(startCircleX + endCircleX) / 2, (startCircleY + endCircleY) / 2,
(float) (startCircleX - curRadius * sin), (float) (startCircleY - curRadius * cos)
);
canvas.drawPath(path, paint);
} } else { // 非触摸状态
if (curRadius > 0) {
startCircleX = curRadius;
startCircleY = originHeight - curRadius;
canvas.drawCircle(startCircleX, startCircleY, curRadius, paint);
if (curRadius == originRadius) { // 只有在恢复正常的情况下才显示文字
// 绘制文字
float textH = textFontMetrics.bottom - textFontMetrics.top;
canvas.drawText(text, startCircleX, startCircleY + textH / 2, textPaint);
// canvas.drawText(text, startCircleX, startCircleY, textPaint);
}
} } // Logger.d(TAG, "circleX: " + startCircleX + ", circleY: " + startCircleY + ", curRadius: " + curRadius); } float downX = Float.MAX_VALUE;
float downY = Float.MAX_VALUE; @Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
// Logger.d(TAG, "onTouchEvent: " + event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isTouched = true;
this.setLayoutParams(newLp);
endPoint.x = (int) downX;
endPoint.y = (int) downY; changeViewHeight(this, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
postInvalidate(); downX = event.getX() + location[0];
downY = event.getY() + location[1];
// Logger.d(TAG, String.format("downX: %f, downY: %f", downX, downY)); break;
case MotionEvent.ACTION_MOVE:
// 计算直角边和斜边(用于计算绘制两圆之间的填充去)
triangle.deltaX = event.getX() - downX;
triangle.deltaY = -1 * (event.getY() - downY); // y轴方向相反,所有需要取反
double distance = Math.sqrt(triangle.deltaX * triangle.deltaX + triangle.deltaY * triangle.deltaY);
triangle.hypotenuse = distance;
// Logger.d(TAG, "triangle: " + triangle);
refreshCurRadiusByMoveDistance((int) distance); endPoint.x = (int) event.getX();
endPoint.y = (int) event.getY(); postInvalidate(); break;
case MotionEvent.ACTION_UP:
isTouched = false;
this.setLayoutParams(originLp); if (isArrivedMaxMoved) { // 触发事件
changeViewHeight(this, originWidth, originHeight);
postInvalidate();
if (null != onDraggableFlagViewListener) {
onDraggableFlagViewListener.onFlagDismiss(this);
}
Logger.d(TAG, "触发事件...");
resetAfterDismiss();
} else { // 还原
changeViewHeight(this, originWidth, originHeight);
startRollBackAnimation(500/*ms*/);
} downX = Float.MAX_VALUE;
downY = Float.MAX_VALUE;
break;
} return true;
} /**
* 触发事件之后重置
*/
private void resetAfterDismiss() {
this.setVisibility(GONE);
text = "";
isArrivedMaxMoved = false;
curRadius = originRadius;
postInvalidate();
} /**
* 根据移动的距离来刷新原来的圆半径大小
*
* @param distance
*/
private void refreshCurRadiusByMoveDistance(int distance) {
if (distance > maxMoveLength) {
isArrivedMaxMoved = true;
curRadius = 0;
} else {
isArrivedMaxMoved = false;
float calcRadius = (1 - 1f * distance / maxMoveLength) * originRadius;
float maxRadius = ABTextUtil.dip2px(context, 2);
curRadius = (int) Math.max(calcRadius, maxRadius);
// Logger.d(TAG, "[refreshCurRadiusByMoveDistance]curRadius: " + curRadius + ", calcRadius: " + calcRadius + ", maxRadius: " + maxRadius);
} } /**
* 改变某控件的高度
*
* @param view
* @param height
*/
private void changeViewHeight(View view, int width, int height) {
ViewGroup.LayoutParams lp = view.getLayoutParams();
if (null == lp) {
lp = originLp;
}
lp.width = width;
lp.height = height;
view.setLayoutParams(lp);
} /**
* 回滚状态动画
*/
private ValueAnimator rollBackAnim; private void startRollBackAnimation(long duration) {
if (null == rollBackAnim) {
rollBackAnim = ValueAnimator.ofFloat(curRadius, originRadius);
rollBackAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
curRadius = (int) value;
postInvalidate();
}
});
rollBackAnim.setInterpolator(new BounceInterpolator()); // 反弹效果
rollBackAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
DraggableFlagView.this.clearAnimation();
}
});
}
rollBackAnim.setDuration(duration);
rollBackAnim.start();
} /**
* 计算四个坐标的三角边关系
*/
class Triangle {
double deltaX;
double deltaY;
double hypotenuse; @Override
public String toString() {
return "Triangle{" +
"deltaX=" + deltaX +
", deltaY=" + deltaY +
", hypotenuse=" + hypotenuse +
'}';
}
} public String getText() {
return text;
} public void setText(String text) {
this.text = text;
postInvalidate();
}
}

[Android]仿新版QQ的tab下面拖拽标记为已读的效果的更多相关文章

  1. Android 仿微信朋友圈发表图片拖拽和删除功能

    朋友圈实现原理 我们使用 Android Device Monitor 来分析朋友圈发布图片的界面实现原理.如果需要分析其他应用的界面实现也是采用这种方法哦. 打开 Android Device Mo ...

  2. Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发"鼻涕"下拉粘连效果

    前言 接着上一期Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件 的博客开始,同样,在开始前我们先来看一下目标效果: 下面上一下本章需要实现的效果图: 大家看到这个效果 ...

  3. Android学习系列(11)--App列表之拖拽ListView(下)

    接着上篇Android学习系列(10)--App列表之拖拽ListView(上)我们继续实现ListView的拖拽效果. 7.重写onTouchEvent()方法.     在这个方法中我们主要是处理 ...

  4. reactnative实现qq聊天消息气泡拖拽消失效果

    前言(可跳过) 我在开发自己的APP时遇到了一个类似于qq聊天消息气泡拖拽消息的需求,因为在网上没有找到相关的组件,所以自己动手实现了一下 需求:对聊天消息气泡拖拽到一定长度松开时该气泡会消失(可自行 ...

  5. Android仿微信QQ等实现锁屏消息提醒

    demo代码如下: import android.content.Intent; import android.os.Bundle; import android.support.v7.app.App ...

  6. Android6.0 源码修改之 仿IOS添加全屏可拖拽浮窗返回按钮

    前言 之前写过屏蔽系统导航栏功能的文章,具体可看Android6.0 源码修改之屏蔽导航栏虚拟按键(Home和RecentAPP)/动态显示和隐藏NavigationBar 在某些特殊定制的版本中要求 ...

  7. Android学习系列(12)--App列表之拖拽GridView

    根据前面文章中ListView拖拽的实现原理,我们也是很容易实现推拽GridView的,下面我就以相同步骤实现基本的GridView拖拽效果.     因为GridView不用做分组处理,代码处理起来 ...

  8. Android学习系列(10)--App列表之拖拽ListView(上)

     研究了很久的拖拽ListView的实现,受益良多,特此与尔共飨.      鉴于这部分内容网上的资料少而简陋,而具体的实现过程或许对大家才有帮助,为了详尽而不失真,我们一步一步分析,分成两篇文章. ...

  9. android开发技巧——仿新版QQ锁屏下弹窗

    新版的qq,可以在锁屏下弹窗显示qq消息,正好目前在做的项目也需要这一功能.经过各种试验和资料查找,终于实现,过程不难,但是却有一些地方需要注意. 下面是实现过程. 1.使用Activity,而不是V ...

随机推荐

  1. Theano入门神经网络(四)

    这一部分主要介绍用Theano 实现softmax函数. 在多分类任务中经常用到softmax函数,首先上几个投影片解释一下 假设目标输出是y_hat ,激活函数后的Relu y 一个是1.2 一个是 ...

  2. js基础篇——encodeURI 和encodeURIComponent

    转自zccst的又一次掉进encodeURIComponent的坑里了 问题: ajax.get ( url+'?k1'=v1+'&k2'=v2+'&k3'=v3, ... ); 由于 ...

  3. SQL Server安全(7/11):使用跨数据库所有权链接(Cross-Database Ownership Chaining)的跨数据库安全

    在保密你的服务器和数据,防备当前复杂的攻击,SQL Server有你需要的一切.但在你能有效使用这些安全功能前,你需要理解你面对的威胁和一些基本的安全概念.这篇文章提供了基础,因此你可以对SQL Se ...

  4. JavaScript之旅(三)

    JavaScript之旅(三) 三.函数 在JavaScript中,定义函数的方式如下: function abs(x) { ... return ...; } 如果没有return,返回结果为und ...

  5. Javascript中的正则表达式

    Javascript中的正则表达式 刚开始接触正则表达式的时候,觉得这是个很死板的东西(没办法,计算机不能像人眼一样能很快的辨认出我们需要的结果,它需要一定的规则来对它进行限制),了解的越多,发现这个 ...

  6. C#密封类

    密封类 密封类使用sealed修饰符声明. 密封类中不可能有抽象方法[因为:抽象方法必须在抽象类中,而抽象类不能是密封的或者是静态的,也就是说abstract 和sealed不能同时修饰一个类]   ...

  7. 【转】 NoSQL初探之人人都爱Redis:(4)Redis主从复制架构初步探索

    一.主从复制架构简介 通过前面几篇的介绍中,我们都是在单机上使用Redis进行相关的实践操作,从本篇起,我们将初步探索一下Redis的集群,而集群中最经典的架构便是主从复制架构.那么,我们首先来了解一 ...

  8. C语言学习012:将代码文件分成多个文件

    如果将所有的代码都写到一个文件中,当对于小项目来说不会有什么问题,但是当它一个很大的工程的时候,如果将所有代码都写到一个文件中,不但开发起来很困难,维护更是头疼,所以我们应该将代码按不同的功能分别建立 ...

  9. C语言学习004:数组与指针

    在C语言中,字符串实际上就是字符数组,在内存中字符串"Shatner"存储的形式是这样的

  10. Nlog 记录日志到 sqlite

    最近研究了一下Nlog这个日志框架,这里记录一下如何将日志写到sqlite中. 第一步:使用NuGet获取Nlog和Sqlite 第二步:在sqlite中创建一个database,这里我用了SQLit ...