仿QQ未读消息拖拽删除粘性效果

来源:互联网 发布:28岁程序员 压力大 编辑:程序博客网 时间:2024/04/30 23:47

1. activity_main中的代码

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="@android:color/white"    android:orientation="vertical">    <RelativeLayout        android:id="@+id/rl"        android:layout_width="match_parent"        android:layout_height="60dp">        <com.wj.sticky.StickyFlagView            android:id="@+id/sticky_view"            android:layout_width="40dp"            android:layout_height="40dp"            android:layout_centerVertical="true"            app:flagRadius="10dp"            app:flagTextSize="15sp" />        <TextView            android:layout_width="match_parent"            android:layout_height="match_parent"            android:gravity="center"            android:text="This is a RelativeLayout"            android:textSize="20sp" />    </RelativeLayout>    <View        android:layout_width="match_parent"        android:layout_height="1dp"        android:background="@android:color/darker_gray"/>    <FrameLayout        android:id="@+id/fl"        android:layout_width="match_parent"        android:layout_height="60dp">        <TextView            android:layout_width="match_parent"            android:layout_height="match_parent"            android:gravity="center"            android:text="This is a FrameLayout"            android:textSize="20sp" />        <com.wj.sticky.StickyFlagView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_marginLeft="10dp"            android:layout_gravity="center_vertical"            app:flagColor="#f74c31"            app:flagDrawable="@drawable/bubble" />    </FrameLayout>    <View        android:layout_width="match_parent"        android:layout_height="1dp"        android:background="@android:color/darker_gray"/></LinearLayout></span>

局中的StickyFlagView是一个封装好的未读消息拖拽控件,在这里我们先来了解一下它的xml属性。

(1)flagColor :标记的颜色(包括黏着线和黏着点)
(2)flagTextColor:标记的文本颜色
(3)flagTextSize:标记的文本大小
(4)flagRadius:标记的半径,设置此属性后,将按照给定的半径绘制一个标记圆点。如果同时设置了flagDrawable属性,则此属性失效。
(5)flagDrawable:为标记指定图片,设置此属性后,将绘制一个图片标记。
(6)maxStickRadius:黏着点的最大黏着半径
(7)minStickRadius:黏着点的最小黏着半径
(8)maxDistance:标记最大的拖拽距离

2. MainActivity.Java文件

public class MainActivity extends AppCompatActivity {      @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);          final StickyFlagView sfv = (StickyFlagView) findViewById(R.id.sticky_view);          sfv.setOnFlagDisappearListener(new StickyFlagView.OnFlagDisappearListener() {              @Override              public void onFlagDisappear(StickyFlagView view) {                  Toast.makeText(MainActivity.this, "Flag have disappeared.", Toast.LENGTH_SHORT).show();              }          });          sfv.setFlagText("6");      }  }

设置了未读消息标记的文本和删除后的回调监听。

3. StickyFlagView的具体实现

private Context context;  private ViewGroup parent;  private ViewGroup.LayoutParams originalLp; // view原始layout  private int[] originalLocation; // view原始的location  private int originalWidth; // view原始的宽度  private int originalHeight; // view原始的高度  private float stickRadius; // 黏贴半径  private int flagColor; // 标记颜色  private int flagTextColor; // 标记文本颜色  private float maxDragDistance; // 最大拖拽距离  private String flagText; // 标记文本  private float flagTextSize; // 标记文本大小  private float flagRadius; // 标记半径  private Bitmap flagBitmap; // 标记图片  private float maxStickRadius; // 最大黏贴半径  private float minStickRadius; // 最小黏贴半径  private float rate = 0.8f;  private boolean isFirstSizeChange = true;  private boolean isTouched;  private boolean isReachLimit; // 是否达到最大拖拽距离  private boolean isRollBackAnimating; // 回滚动画是否在执行  private boolean isDisappearAnimating; // 消失动画是否在执行  private boolean isFlagDisappear; // 标记是否消失  private boolean isViewLoadFinish; // view是否加载完毕  private boolean isViewInWindow; // view是否在window中  private int which;  private List<Integer> disappearRes;  private PointF stickPoint; // 黏贴点  private PointF dragFlagPoint; // 拖拽标记点  private Paint flagPaint; // 标记画笔  private Paint flagTextPaint; // 标记文本画笔  private Path flagPath;  private OnFlagDisappearListener listener;  private WindowManager windowManager;  public StickyFlagView(Context context) {      super(context);      this.context = context;      init();  }  public StickyFlagView(Context context, AttributeSet attrs) {      super(context, attrs);      this.context = context;      initViewProperty(context, attrs);      init();  }  public StickyFlagView(Context context, AttributeSet attrs, int defStyleAttr) {      super(context, attrs, defStyleAttr);      this.context = context;      initViewProperty(context, attrs);      init();  }  /**  * 初始化view属性  */  private void initViewProperty(Context context, AttributeSet attrs) {      TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.StickyFlagView);      flagColor = typedArray.getColor(R.styleable.StickyFlagView_flagColor, Color.RED);      flagTextColor = typedArray.getColor(R.styleable.StickyFlagView_flagTextColor, Color.WHITE);      flagTextSize = typedArray.getDimension(R.styleable.StickyFlagView_flagTextSize, ScreenUtils.spTopx(context, 12));      maxDragDistance = typedArray.getDimension(R.styleable.StickyFlagView_maxDistance, ScreenUtils.getScreenHeight(context) / 6);      minStickRadius = typedArray.getDimension(R.styleable.StickyFlagView_minStickRadius, ScreenUtils.dpToPx(context, 2));      flagRadius = typedArray.getDimension(R.styleable.StickyFlagView_flagRadius, ScreenUtils.dpToPx(context, 10));      maxStickRadius = typedArray.getDimension(R.styleable.StickyFlagView_maxStickRadius, flagRadius * rate);      Drawable flagDrawable = typedArray.getDrawable(R.styleable.StickyFlagView_flagDrawable);      if (flagDrawable != null) {          flagBitmap = ((BitmapDrawable) flagDrawable).getBitmap();      }       typedArray.recycle();  }  private void init() {      // 处理onDraw方法不执行的问题      setWillNotDraw(false);      // 这些默认值是为第一个构造函数准备的      if (flagColor == 0) {          flagColor = Color.RED;      }      if (flagTextColor == 0) {          flagTextColor = Color.WHITE;      }      if (flagTextSize == 0) {          flagTextSize = ScreenUtils.spTopx(context, 12);      }      if (flagRadius == 0) {          flagRadius = ScreenUtils.dpToPx(context, 10);      }      if (maxDragDistance == 0) {          maxDragDistance = ScreenUtils.getScreenHeight(context) / 6;      }      if (minStickRadius == 0) {          minStickRadius = ScreenUtils.dpToPx(context, 2);      }      if (maxStickRadius == 0) {          maxStickRadius = flagRadius * rate;      }      originalLocation = new int[2];      stickPoint = new PointF();      dragFlagPoint = new PointF();      flagPath = new Path();      flagPaint = new Paint();      flagPaint.setAntiAlias(true);      flagPaint.setColor(flagColor);      flagTextPaint = new Paint();      flagTextPaint.setAntiAlias(true);      flagTextPaint.setColor(flagTextColor);      flagTextPaint.setTextSize(flagTextSize);      windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);  }  

上面的代码主要是初始化一些成员变量,拿到自定属性值,设置一些默认值。需要注意的是init()方法中setWillNotDraw(false),这段代码不能缺少,在高版本SDK中,invalid方法可能不能促使view的onDraw方法执行。大家可能注意到成员变量中有这么两个属性:parent和originalLp,这两个属性的作用是什么呢?大家可以想象一下QQ的未读消息拖拽效果,可以从自己所在的item中拖出来,并不受父控件大小的束缚。这种效果要借助WindowManager把StickyFlagView加入到window中,并把StickyFlagView的宽高设置为match。但是一个孩子不能有两个父亲,所以我们要先把view从父控件中移除,再把view添加到window中,等到拖拽效果完成之后,再把view从window中移除,然后还给原来的父亲。parent属性就用来保存view本来的父控件,originalLp用来保存view的布局参数。那么在什么时机把view加入window,又在什么时机把view加入原来的父控件呢,让我们接着看代码。

@Override  public boolean onTouchEvent(MotionEvent event) {      if (!isViewLoadFinish || isRollBackAnimating || isDisappearAnimating || isFlagDisappear) {          return true;      }      switch (event.getAction()) {          case MotionEvent.ACTION_DOWN:              isTouched = true;              addViewInWindow();              break;          case MotionEvent.ACTION_MOVE:              dragFlagPoint.x = event.getRawX();              dragFlagPoint.y = event.getRawY() - ScreenUtils.getStatusHeight(context);              double distance = Math.sqrt(Math.pow(dragFlagPoint.y - stickPoint.y, 2) + Math.pow(dragFlagPoint.x - stickPoint.x, 2));              if (distance > maxDragDistance) {                  isReachLimit = true;              } else {                  isReachLimit = false;                  stickRadius = (float) (maxStickRadius * (1 - distance / maxDragDistance));                  stickRadius = stickRadius < minStickRadius ? minStickRadius : stickRadius;              }              postInvalidate();              break;          case MotionEvent.ACTION_UP:              isTouched = false;              if (isReachLimit) {                  launchDisappearAnimation(1000);                  if (listener != null) {                      listener.onFlagDisappear(this);                  }              } else {                  launchRollBackAnimation(300);              }              break;      }      return true;  }

这里重写了view的onTouchEvent方法,在down事件触发时调用addViewInWindow方法,这个方法的作用就是把view从当前的父控件中移除,然后加入到window中,view的大小充满整个window,这样我们就拥有了一个面积足够大的悬浮画板,以便我们随心所欲的在上面进行绘制。
当move事件触发时,我们记录了拖拽点dragFlagPoint,这个坐标是用于不断重绘标记的位置。获得拖拽点坐标时,使用了event.getRawY(),然后减去状态栏的高度。一开始我使用的是event.getY(),不过拿到的坐标点出现了极大的误差,按理说view的大小是充满window的,使用getY和getRawY应该只是一个状态栏高度的差别。我也不知道这是为什么,有知道的朋友可以告诉我一下。好了,让我们继续,得到dragFlagPoint后,我们计算了它与黏着点(stickPoint)的距离distance,stickPoint的初始值是view还在原父控件中时的自身的中心点,当view加入到window时stickPoint的坐标要修正一下,不然view的大小发生了变化,再按原来的坐标绘制,黏着点肯定要跑偏了,这部分的逻辑我们稍后会讲。isReachLimit是个标识位,当distance大于最大拖拽距离时把它设为true,否则设为false,并根据distance改变黏着点半径的大小。
当up事件触发时,我们会判断是否达到了极限距离,如果达到了调用launchDisappearAnimation方法,这个方法是标记的一个删除时的动画,否则执行标记的回弹动画。两个动画执行完毕后都会做同一件事情,那就是调用restoreView方法,把view从window中移除,然后把view还给原来的父亲。
上面提到了addViewInWindow和restoreView方法,下面贴出它们的代码

private void addViewInWindow() {      if (isViewLoadFinish && !isViewInWindow) {          if (parent != null) {              // 将view从它的父控件中移除              parent.removeView(this);          }          WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();          layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;          layoutParams.format = PixelFormat.TRANSPARENT;          layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;          layoutParams.gravity = Gravity.START | Gravity.TOP;          layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;          layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT;          layoutParams.x = 0;          layoutParams.y = 0;          if (windowManager != null) {              // 将view加入window              windowManager.addView(this, layoutParams);              post(new Runnable() {                  @Override                  public void run() {                      isViewInWindow = true;                  }              });          }      }  }  private void restoreView() {      if (isViewLoadFinish) {          // 还原黏贴半径          stickRadius = flagRadius > maxStickRadius ? maxStickRadius : flagRadius * rate;          isReachLimit = false;          if (windowManager != null && isViewInWindow) {              // 把view从window中移除              windowManager.removeView(this);              isViewInWindow = false;              if (parent != null) {                  parent.addView(this, originalLp);                  // 在高版本的SDK上,没有这段代码,view可能不会刷新                  post(new Runnable() {                      @Override                      public void run() {                          parent.invalidate();                      }                  });              }          }      } else {          post(new Runnable() {              @Override              public void run() {                  restoreView();              }          });      }  }

上面代码中的isInWindow是为了防止不断触发down事件,造成view重复加入window发生异常。isViewLoadFinish是判断view是否还在加载中,我们知道一次点击事件就是一个down和一个up,down的时候我们把view加入window,up时我们把view加入parent,view的加载需要时间,windowManager.addView()调用后,view开始加载,在加载过程中,restoreView方法调用,如果没有isViewLoadFinish这个判断,那么执行windowManner.removeView()就会出现问题。
parent.addView()和windowManager.addView()都会触发以下三个方法执行

@Override  public void setLayoutParams(ViewGroup.LayoutParams params) {      super.setLayoutParams(params);      isViewLoadFinish = false;      this.post(new Runnable() {          @Override          public void run() {              isViewLoadFinish = true;          }      });  }  @Override  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {      int width = 0;      int widthMode = MeasureSpec.getMode(widthMeasureSpec);      if (widthMode == MeasureSpec.UNSPECIFIED) {          if (flagBitmap == null) {              width = (int) ScreenUtils.dpToPx(context, 20);          } else {              width = flagBitmap.getWidth();          }      } else if (widthMode == MeasureSpec.EXACTLY){          width = MeasureSpec.getSize(widthMeasureSpec);      } else if (widthMode == MeasureSpec.AT_MOST) {          if (flagBitmap == null) {              width = (int) ScreenUtils.dpToPx(context, 20);          } else {              width = flagBitmap.getWidth();          }      }      int height = 0;      int heightMode = MeasureSpec.getMode(heightMeasureSpec);      if (heightMode == MeasureSpec.UNSPECIFIED) {          if (flagBitmap == null) {              height = (int) ScreenUtils.dpToPx(context, 20);          } else {              height = flagBitmap.getHeight();          }      } else if (heightMode == MeasureSpec.EXACTLY){          height = MeasureSpec.getSize(heightMeasureSpec);      } else if (heightMode == MeasureSpec.AT_MOST) {          if (flagBitmap == null) {              height = (int) ScreenUtils.dpToPx(context, 20);          } else {              height = flagBitmap.getHeight();          }      }      setMeasuredDimension(width, height);  }  @Override  protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {      super.onSizeChanged(width, height, oldWidth, oldHeight);      if (isFirstSizeChange) {          parent = (ViewGroup) getParent();          // StickyFlagView的父控件只能是RelativeLayout或FrameLayout          if (!(parent instanceof RelativeLayout || parent instanceof FrameLayout)) {              throw new RuntimeException("StickyFlagView can only be placed on the RelativeLayout or FrameLayout.");          }          // 记录view原始layout参数          originalLp = getLayoutParams();          originalWidth = width;          originalHeight = height;          getLocationOnScreen(originalLocation);          originalLocation[1] = originalLocation[1] - ScreenUtils.getStatusHeight(context);          if (flagBitmap == null) {              float radius = Math.min(originalWidth, originalHeight) * 0.5f;              flagRadius = flagRadius > radius ? radius : flagRadius;              stickRadius = flagRadius > maxStickRadius ? maxStickRadius : flagRadius * rate;          } else {              // 黏贴半径不能超过图片宽和高的最小值的一半              flagRadius = Math.min(flagBitmap.getWidth(), flagBitmap.getHeight()) * 0.5f;              stickRadius = maxStickRadius > flagRadius ? flagRadius * rate : maxStickRadius;          }          // 黏贴点在原始view的中心点          stickPoint.set((float) (originalWidth * 0.5), (float) (originalHeight * 0.5));          isFirstSizeChange = false;      } else {          // view的size改变之后,修正黏贴点坐标          if (originalWidth == width && originalHeight == height) {              stickPoint.set((float) (originalWidth * 0.5), (float) (originalHeight * 0.5));          } else {              stickPoint.x += originalLocation[0];              stickPoint.y += originalLocation[1];          }      }      dragFlagPoint.x = stickPoint.x;      dragFlagPoint.y = stickPoint.y;  }

上面三个方法的调用顺序是setLayoutParams,onMeasure,onSizeChanged 大家可以看到isViewLoadFinish的值是在setLayoutParams中设置的,post方法将一个Runnable加入队列,当view加载完毕后,队列中的runnable会依次执行。onMeasure方法里面的代码应该很好理解,当指定了StickyFlagView的宽高后,我们就用指定的宽高,如果没有我们就给一个默认的。在onSizeChanged方法中,大家可以看到parent和originalLp是在size第一次被改变是初始化的,与此同时还记录了view在屏幕中的位置,记录这个位置是为了在view加入window后,修正黏着点的位置,黏着点的初始位置是view的中心点,下面上一张图解释一下

参考; http://www.bubuko.com/infodetail-1092644.html

0 0
原创粉丝点击