Android custom controls: QQ-like unread message drag effect

Android custom controls: QQ-like unread message drag effect

QQ's unread messages are a fun effect. Taking advantage of the recent time, after referring to some online information, this time I implemented a drag-and-drop red dot that imitates QQ's unread messages:

First, let's start with the most basic principles and look at a picture:

How to draw this picture? Actually, we draw two circles first, and then connect the tangent points of the two circles through Bezier curves to achieve this effect. As for the concept of Bezier curves, I won't explain it here. You can find it by searching Baidu.

How to calculate the tangent points? Let's review some junior high school math knowledge. After looking at this graph, it should be easy to find the four tangent points.

Now the idea is very clear, let's start according to our idea.

First, we calculate the tangent point and the tool class of each coordinate point

  1. public class GeometryUtils {
  2. /**
  3. * As meaning of method name .
  4. * Get the distance between two points
  5. * @param p0
  6. * @param p1
  7. * @return  
  8. */
  9. public   static   float getDistanceBetween2Points(PointF p0, PointF p1) {
  10. float distance = ( float ) Math.sqrt(Math.pow(p0.y - p1.y, 2) + Math.pow(p0.x - p1.x, 2));
  11. return distance;
  12. }
  13.  
  14. /**
  15. * Get middle point between p1 and p2.
  16. * Get the midpoint of the line connecting two points
  17. * @param p1
  18. * @param p2
  19. * @return  
  20. */
  21. public   static PointF getMiddlePoint(PointF p1, PointF p2) {
  22. return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f);
  23. }
  24.  
  25. /**
  26. * Get point between p1 and p2 by percent.
  27. * Get the coordinates of a point between two points based on the percentage
  28. * @param p1
  29. * @param p2
  30. * @param percent
  31. * @return  
  32. */
  33. public   static PointF getPointByPercent(PointF p1, PointF p2, float percent) {
  34. return new PointF(evaluateValue(percent, p1.x, p2.x), evaluateValue(percent, p1.y, p2.y));
  35. }
  36.  
  37. /**
  38. * Calculate the value of the fraction position from start to end according to the division value . The range of fraction is 0 -> 1
  39. * @param fraction
  40. * @param start
  41. * @param end  
  42. * @return  
  43. */
  44. public   static   float evaluateValue( float fraction, Number start, Number end ){
  45. return start.floatValue() + ( end .floatValue() - start.floatValue()) * fraction;
  46. }
  47.  
  48. /**
  49. * Get the point of intersection between circle and line.
  50. * Get the intersection point of the line with slope lineK and the circle passing through the specified center.
  51. *
  52. * @param pMiddle The circle center point.
  53. * @param radius The circle radius.
  54. * @param lineK The slope of line which cross the pMiddle.
  55. * @return  
  56. */
  57. public   static PointF[] getIntersectionPoints(PointF pMiddle, float radius, Double lineK) {
  58. PointF[] points = new PointF[2];
  59.  
  60. float radian, xOffset = 0, yOffset = 0;
  61. if(lineK != null ){
  62. radian= ( float ) Math.atan(lineK);
  63. xOffset = ( float ) (Math.sin(radian) * radius);
  64. yOffset = ( float ) (Math.cos(radian) * radius);
  65. } else {
  66. xOffset = radius;
  67. yOffset = 0;
  68. }
  69. points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset);
  70. points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset);
  71.  
  72. return points;
  73. }
  74. }

Then let's take a look at our core drawing code. The code comments are quite complete, so I won't explain it here.

  1. /**
  2. * Draw the Bezier curve part and the fixed circle
  3. *
  4. * @param canvas
  5. */
  6. private void drawGooPath(Canvas canvas) {
  7. Path path = new Path();
  8. //1. Calculate the radius of the fixed circle based on the current distance between the centers of the two circles
  9. float distance = ( float ) GeometryUtils.getDistanceBetween2Points(mDragCenter, mStickCenter);
  10. stickCircleTempRadius = getCurrentRadius(distance);
  11.  
  12. //2. Calculate the dragLineK (opposite side vs adjacent side) of the perpendicular line passing through the center of the two circles. Find the coordinates of the four intersection points
  13. float xDiff = mStickCenter.x - mDragCenter.x;
  14. Double dragLineK = null ;
  15. if (xDiff != 0) {
  16. dragLineK = ( double ) ((mStickCenter.y - mDragCenter.y) / xDiff);
  17. }
  18.  
  19. // Get the intersection points of the two circles with the perpendicular lines passing through the center of the two circles (the two perpendicular lines are parallel, so dragLineK is equal).
  20. PointF[] dragPoints = GeometryUtils.getIntersectionPoints(mDragCenter, dragCircleRadius, dragLineK);
  21. PointF[] stickPoints = GeometryUtils.getIntersectionPoints(mStickCenter, stickCircleTempRadius, dragLineK);
  22.  
  23. //3. Use the 0.618 point of the line connecting the two circles as the control point of the Bezier curve. (Choose a control point near the middle point)
  24. PointF pointByPercent = GeometryUtils.getPointByPercent(mDragCenter, mStickCenter, 0.618f);
  25.  
  26. // Draw two circles to connect and close
  27. path.moveTo(( float ) stickPoints[0].x, ( float ) stickPoints[0].y);
  28. path.quadTo(( float ) pointByPercent.x, ( float ) pointByPercent.y,
  29. ( float ) dragPoints[0].x, ( float ) dragPoints[0].y);
  30. path.lineTo(( float ) dragPoints[1].x, ( float ) dragPoints[1].y);
  31. path.quadTo(( float ) pointByPercent.x, ( float ) pointByPercent.y,
  32. ( float ) stickPoints[1].x, ( float ) stickPoints[1].y);
  33. canvas.drawPath(path, mPaintRed);
  34. // Draw a fixed circle
  35. canvas.drawCircle(mStickCenter.x, mStickCenter.y, stickCircleTempRadius, mPaintRed);
  36. }

At this point, we have implemented the core code for drawing, and then we add a listener for touch events to dynamically update the center position of the dragPoint and the radius of the stickPoint. When the hand is raised, add an attribute animation to achieve a rebound effect.

  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3. switch (MotionEventCompat.getActionMasked(event)) {
  4. case MotionEvent.ACTION_DOWN: {
  5. isOutOfRange = false ;
  6. updateDragPointCenter(event.getRawX(), event.getRawY());
  7. break;
  8. }
  9. case MotionEvent.ACTION_MOVE: {
  10. //If the distance between the two circles is greater than the maximum distance mMaxDistance, execute the drag end animation
  11. PointF p0 = new PointF(mDragCenter.x, mDragCenter.y);
  12. PointF p1 = new PointF(mStickCenter.x, mStickCenter.y);
  13. if (GeometryUtils.getDistanceBetween2Points(p0, p1) > mMaxDistance) {
  14. isOutOfRange = true ;
  15. updateDragPointCenter(event.getRawX(), event.getRawY());
  16. return   false ;
  17. }
  18. updateDragPointCenter(event.getRawX(), event.getRawY());
  19. break;
  20. }
  21. case MotionEvent.ACTION_UP: {
  22. handleActionUp();
  23. break;
  24. }
  25. default : {
  26. isOutOfRange = false ;
  27. break;
  28. }
  29. }
  30. return   true ;
  31. }
  32.  
  33. /**
  34. * Gesture lift action
  35. */
  36. private void handleActionUp() {
  37. if (isOutOfRange) {
  38. // When the dragPoint range has exceeded mMaxDistance, and then drag the dragPoint back to the mResetDistance range
  39. if (GeometryUtils.getDistanceBetween2Points(mDragCenter, mStickCenter) < mResetDistance) {
  40. //reset
  41. return ;
  42. }
  43. //dispappear
  44. } else {
  45. //When the finger is lifted, the bounce animation
  46. mAnim = ValueAnimator.ofFloat(1.0f);
  47. mAnim.setInterpolator(new OvershootInterpolator(5.0f));
  48.  
  49. final PointF startPoint = new PointF(mDragCenter.x, mDragCenter.y);
  50. final PointF endPoint = new PointF(mStickCenter.x, mStickCenter.y);
  51. mAnim.addUpdateListener(new AnimatorUpdateListener() {
  52. @Override
  53. public void onAnimationUpdate(ValueAnimator animation) {
  54. float fraction = animation.getAnimatedFraction();
  55. PointF pointByPercent = GeometryUtils.getPointByPercent(startPoint, endPoint, fraction);
  56. updateDragPointCenter(( float ) pointByPercent.x, ( float ) pointByPercent.y);
  57. }
  58. });
  59. mAnim.addListener(new AnimatorListenerAdapter() {
  60. @Override
  61. public void onAnimationEnd(Animator animation) {
  62. //reset
  63. }
  64. });
  65.  
  66. if (GeometryUtils.getDistanceBetween2Points(startPoint, endPoint) < 10) {
  67. mAnim.setDuration(100);
  68. } else {
  69. mAnim.setDuration(300);
  70. }
  71. mAnim.start();
  72. }
  73. }

At this point, the core code of our drag and drop has basically been completed, and the actual effect is as follows:

Now that the drawing of the little red dot is basically over, we have to think about the real difficulty. That is, how to apply the GooView we mentioned above to practice? Looking at the actual effect, our little red dot is placed inside the listView. If this is the case, it means that the drag range of our GooView will definitely not exceed the area of ​​the parent control item.

So how do we make the little red dot drag freely across the entire screen? Let's sort out our ideas here.

1. First put a small red dot in the item layout of listView.

2. When we touch this little red dot, hide this little red dot, and then initialize a GooView according to the position of the little red dot in our layout and add it to the WindowManager, so that GooView can be dragged full screen.

3. When adding GooView to WindowManager, record the position of the initial red dot stickPoint, and then determine the next logic based on whether the positions of stickPoint and dragPoint exceed our disappearance limit.

4. Depending on the final state of GooView, display the rebound or disappearance animation.

Now that we have the idea, let's move on to the code. According to the first step, we complete the item layout of listView.

  1. <?xml version= "1.0" encoding= "utf-8" ?>
  2. <RelativeLayout xmlns:android= "http://schemas.android.com/apk/res/android"  
  3. android:layout_width= "match_parent"  
  4. android:layout_height= "80dp"  
  5. android:minHeight= "80dp" >
  6.  
  7. <ImageView
  8. android:id= "@+id/iv_head"  
  9. android:layout_width= "50dp"  
  10. android:layout_height= "50dp"  
  11. android:layout_centerVertical= "true"  
  12. android:layout_marginLeft= "20dp"  
  13. android:src= "@mipmap/head" />
  14.  
  15. <TextView
  16. android:id= "@+id/tv_content"  
  17. android:layout_width= "wrap_content"  
  18. android:layout_height= "50dp"  
  19. android:layout_centerVertical= "true"  
  20. android:gravity= "center"  
  21. android:layout_marginLeft= "20dp"  
  22. android:layout_toRightOf= "@+id/iv_head"  
  23. android:text= "content - "  
  24. android:textSize= "25sp" />
  25.  
  26. <LinearLayout
  27. android:id= "@+id/ll_point"  
  28. android:layout_width= "80dp"  
  29. android:layout_height= "80dp"  
  30. android:layout_alignParentEnd= "true"  
  31. android:layout_alignParentRight= "true"  
  32. android:layout_alignParentTop= "true"  
  33. android:gravity= "center" >
  34.  
  35. <TextView
  36. android:id= "@+id/point"  
  37. android:layout_width= "wrap_content"  
  38. android:layout_height= "18dp"  
  39. android:background= "@drawable/red_bg"  
  40. android:gravity= "center"  
  41. android:singleLine= "true"  
  42. android:textColor= "@android:color/white"  
  43. android:textSize= "12sp" />
  44. </LinearLayout>
  45. </RelativeLayout>

The effect is as follows. It should be noted that compared with the real experience of QQ, when clicking around the red dot, you can directly drag the red dot. Considering that the click range of the red dot is relatively small, a parent layout with a width and height of 80dp is added to the red dot. Then we change the touch red dot event to touch the red dot parent layout, so that as long as we click the parent layout range of the red dot, GooView will be added to the WindowManager.

In the second step, we complete the code to add GooView to WindowManager.

Since our GooView is initially added from the touch event of the red dot in the listViewItem, we first complete the implementation of the listView adapter.

  1. public class GooViewAapter extends BaseAdapter {
  2. private Context mContext;
  3. //Record the position that has been removed
  4. private HashSet< Integer > mRemoved = new HashSet< Integer >();
  5. private List<String> list = new ArrayList<String>();
  6.  
  7. public GooViewAapter(Context mContext, List<String> list) {
  8. super();
  9. this.mContext = mContext;
  10. this.list = list;
  11. }
  12.  
  13. @Override
  14. public   int getCount() {
  15. return list.size () ;
  16. }
  17.  
  18. @Override
  19. public Object getItem( int position) {
  20. return list.get(position);
  21. }
  22.  
  23. @Override
  24. public long getItemId( int position) {
  25. return position;
  26. }
  27.  
  28. @Override
  29. public   View getView(final int position, View convertView, ViewGroup parent) {
  30. if (convertView == null ) {
  31. convertView = View .inflate(mContext, R.layout.list_item_goo, null );
  32. }
  33. ViewHolder holder = ViewHolder.getHolder(convertView);
  34. holder.mContent.setText(list.get(position));
  35. //item fixed red dot layout
  36. LinearLayout pointLayout = holder.mPointLayout;
  37. //item fixed red dot
  38. final TextView point = holder.mPoint;
  39.  
  40. boolean visiable = !mRemoved. contains (position);
  41. pointLayout.setVisibility(visiable ? View .VISIBLE : View .GONE);
  42. if (visiable) {
  43. point.setText(String.valueOf(position));
  44. pointLayout.setTag(position);
  45. GooViewListener mGooListener = new GooViewListener(mContext, pointLayout) {
  46. @Override
  47. public void onDisappear(PointF mDragCenter) {
  48. super.onDisappear(mDragCenter);
  49. mRemoved.add (position);
  50. notifyDataSetChanged();
  51. Utils.showToast(mContext, "position " + position + " disappear." );
  52. }
  53.  
  54. @Override
  55. public void onReset(boolean isOutOfRange) {
  56. super.onReset(isOutOfRange);
  57. notifyDataSetChanged();//Refresh ListView
  58. Utils.showToast(mContext, "position " + position + " reset." );
  59. }
  60. };
  61. //Listen for all touch events in the point parent layout
  62. pointLayout.setOnTouchListener(mGooListener);
  63. }
  64. return convertView;
  65. }
  66.  
  67. static class ViewHolder {
  68.  
  69. public ImageView mImage;
  70. public TextView mPoint;
  71. public LinearLayout mPointLayout;
  72. public TextView mContent;
  73.  
  74. public ViewHolder( View convertView) {
  75. mImage = (ImageView) convertView.findViewById(R.id.iv_head);
  76. mPoint = (TextView) convertView.findViewById(R.id.point);
  77. mPointLayout = (LinearLayout) convertView.findViewById(R.id.ll_point);
  78. mContent = (TextView) convertView.findViewById(R.id.tv_content);
  79. }
  80.  
  81. public   static ViewHolder getHolder( View convertView) {
  82. ViewHolder holder = (ViewHolder) convertView.getTag();
  83. if (holder == null ) {
  84. holder = new ViewHolder(convertView);
  85. convertView.setTag(holder);
  86. }
  87. return holder;
  88. }
  89. }
  90. }

Since listview needs to know the status of GooView, we add an interface in GooView for listview to callback and process subsequent logic.

  1. interface OnDisappearListener {
  2. /**
  3. * GooView Disapper
  4. *
  5. * @param mDragCenter
  6. */
  7. void onDisappear(PointF mDragCenter);
  8.  
  9. /**
  10. * GooView onReset
  11. *
  12. * @param isOutOfRange
  13. */
  14. void onReset(boolean isOutOfRange);
  15. }

Create a new class that implements the OnTouchListener and OnDisappearListener methods, and then set this implementation class to the red dot Layout in the item.

  1. public class GooViewListener implements OnTouchListener, OnDisappearListener {
  2.  
  3. private WindowManager mWm;
  4. private WindowManager.LayoutParams mParams;
  5. private GooView mGooView;
  6. private View pointLayout;
  7. private int number;
  8. private final Context mContext;
  9.  
  10. private Handler mHandler;
  11.  
  12. public GooViewListener(Context mContext, View pointLayout) {
  13. this.mContext = mContext;
  14. this.pointLayout = pointLayout;
  15. this.number = ( Integer ) pointLayout.getTag();
  16.  
  17. mGooView = new GooView(mContext);
  18.  
  19. mWm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
  20. mParams = new WindowManager.LayoutParams();
  21. mParams.format = PixelFormat.TRANSLUCENT; //Make the window support transparency
  22. mHandler = new Handler(mContext.getMainLooper());
  23. }
  24.  
  25. @Override
  26. public boolean onTouch( View v, MotionEvent event) {
  27. int   action = MotionEventCompat.getActionMasked(event);
  28. // When pressed, add the custom View to the WindowManager
  29. if ( action == MotionEvent.ACTION_DOWN) {
  30. ViewParent parent = v.getParent();
  31. // Request its parent View not to intercept Touch events
  32. parent.requestDisallowInterceptTouchEvent( true );
  33.  
  34. int [] points = new int [2];
  35. //Get the position of pointLayout on the screen (the coordinates of the upper left corner of the layout)
  36. pointLayout.getLocationInWindow(points);
  37. //Get the initial center coordinates of the small red dot
  38. int x = points[0] + pointLayout.getWidth() / 2;
  39. int y = points[1] + pointLayout.getHeight() / 2;
  40. // Initialize the information, number and coordinates of the currently clicked item
  41. mGooView.setStatusBarHeight(Utils.getStatusBarHeight(v));
  42. mGooView.setNumber(number);
  43. mGooView.initCenter(x, y);
  44. //Set the current GooView disappearance monitor
  45. mGooView.setOnDisappearListener(this);
  46. // Add the current GooView to WindowManager
  47. mWm.addView(mGooView, mParams);
  48. pointLayout.setVisibility( View .INVISIBLE);
  49. }
  50. // Forward all touch events to GooView for processing
  51. mGooView.onTouchEvent(event);
  52. return   true ;
  53. }
  54.  
  55. @Override
  56. public void onDisappear(PointF mDragCenter) {
  57. //disappear Next step completed
  58. }
  59.  
  60. @Override
  61. public void onReset(boolean isOutOfRange) {
  62. // When the dragPoint bounces back, remove the View and add it again the next time ACTION_DOWN
  63. if (mWm != null && mGooView.getParent() != null ) {
  64. mWm.removeView(mGooView);
  65. }
  66. }
  67. }

In this way, we have basically completed most of the functions. Now there is only one step left, which is the processing after GooView goes out of range and disappears. Here we use a frame animation to complete the explosion effect.

  1. public class BubbleLayout extends FrameLayout {
  2. Context context;
  3.  
  4. public BubbleLayout(Context context) {
  5. super(context);
  6. this.context = context;
  7. }
  8.  
  9. private int mCenterX, mCenterY;
  10.  
  11. public void setCenter( int x, int y) {
  12. mCenterX = x;
  13. mCenterY = y;
  14. requestLayout();
  15. }
  16.  
  17. @Override
  18. protected void onLayout(boolean changed, int   left , int   top , int   right ,
  19. int bottom) {
  20. View child = getChildAt(0);
  21. // Set the View to the specified position
  22. if (child != null && child.getVisibility() != GONE) {
  23. final int width = child.getMeasuredWidth();
  24. final int height = child.getMeasuredHeight();
  25. child.layout(( int ) (mCenterX - width / 2.0f), ( int ) (mCenterY - height / 2.0f)
  26. , ( int ) (mCenterX + width / 2.0f), ( int ) (mCenterY + height / 2.0f));
  27. }
  28. }
  29. }
  30.  
  31. @Override
  32. public void onDisappear(PointF mDragCenter) {
  33. if (mWm != null && mGooView.getParent() != null ) {
  34. mWm.removeView(mGooView);
  35.  
  36. //Play the bubble explosion animation
  37. ImageView imageView = new ImageView(mContext);
  38. imageView.setImageResource(R.drawable.anim_bubble_pop);
  39. AnimationDrawable mAnimDrawable = (AnimationDrawable) imageView
  40. .getDrawable();
  41.  
  42. final BubbleLayout bubbleLayout = new BubbleLayout(mContext);
  43. bubbleLayout.setCenter(( int ) mDragCenter.x, ( int ) mDragCenter.y - Utils.getStatusBarHeight(mGooView));
  44.  
  45. bubbleLayout.addView(imageView, new FrameLayout.LayoutParams(
  46. android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
  47. android.widget.FrameLayout.LayoutParams.WRAP_CONTENT));
  48.  
  49. mWm.addView(bubbleLayout, mParams);
  50.  
  51. mAnimDrawable.start();
  52.  
  53. // After playback ends, delete the bubbleLayout
  54. mHandler.postDelayed(new Runnable() {
  55. @Override
  56. public void run() {
  57. mWm.removeView(bubbleLayout);
  58. }
  59. }, 501);
  60. }
  61. }

***Attached is the complete demo address: https://github.com/Horrarndoo/GooView

<<:  Changes to device identifiers in Android O

>>:  Never been so amazing! Hello, SuperTextView

Recommend

Apple will truly achieve password-free login. How will it do it?

At the Worldwide Developers Conference (WWDC) hel...

Facebook user growth strategy!

Whenever we talk about growth, we have to mention...

How to design an e-commerce new product channel?

Nowadays, with the rapid development of the Inter...

Eat me? Want to eat me again? Eat me again...

Food lovers must have a love-hate relationship wi...