There's a great interaction in Pinterest. Maybe the operation is that on the list page, Item can be selected by pressing a single Item for a long time and making the surrounding items transparent and white. Then the option menu pops up. You can choose the menu by moving your fingers. Let's take a look at a graph to compare the implementation in the simplified version and the effect of Pinterest.
Pinterest is on the right, Jianwoo is on the left. The red of the icon is the theme red of my app. It's a little different from that of Interest. There's not much difference between other imitations and Interest. Here's an interactive animation in Jianwoo.
Is the interaction great? Next, let's discuss the specific implementation ideas, what problems we will encounter and how to solve them.
Think is the most important thing!
Analysis
What are the effects to be achieved?
- 1. After pressing item for a long time, pop up the menu
- 2. After popping up the menu, you can select the menu by touching the screen.
- 3. When the menu arrives at the selection area, the icon should be animated and the title text of the current menu should be displayed.
- 4. Except for the selected area, it should turn white.
- 5. There are exit animations in Title bar and Tab bar after long press, and animations back after release.
Now that we have identified the problem, let's solve it.
Long press Item and pop-up menu
Question: After a long press, the menu pops up. Where does the menu appear? (What should the container control of the menu be) is it on the current Fragment/Activity?
This is the first question. To pop up the menu, the menu can not appear empty. First, we need to determine the container that carries it and add it with the current Activity or Fragment's root layout. OK? Okay, but should we do that? Is that good? The answer is no, why?
Imagine: If the pop-up menu is above the current Activity/Fragment layout, it means that your Activity/Fragment layout has to adapt to the interaction. LineaLayout can't be the root layout of Activity/Fragment, and most importantly, once you decide to add these View elements with Activity/Fragment layout, it means you. This interaction is bound to Activeness / Fragment, and reuse will be very cumbersome! ____________ This is a plan that should never be adopted. It's wrong at first, and it's going to be wrong a lot later.
So what should we use? I first thought about Windows Manager. Why? Because Windows Manager doesn't need Activity as the host, it's free and convenient. By setting Windows Manager. LayoutParams. type reasonably, it can also make the pop-up suspension window do not need permission. It's nice, but there's a century-old bug: if the user sets the suspension window not allowed to pop-up, it will mean that Windows Manager can't use it. In this case, Windows Manager can't use it. Even Toast can't be used! It has to be said that a manufacturer castrates for users, so I implemented the implementation of Dialog, but in fact, whether using WIndowManger or Dialog, it does not affect you to achieve the main functions, because Windows Manager/Dialog only needs you to provide a View to it, and Dialog needs a host activity.
Now that we have identified the container where the pop-up View appears, let's go ahead and solve the next problem. How do we locate the pop-up menu?
This is a long-click pop-up menu. First, set OnTouchListener to the Item view. By listening to ACTION_DOWN, you can get the coordinates x,y in the screen pressed by the user.
In Adapter
getHolder(holder).mImage.setOnTouchListener(listener);
public TouchToSelector onTouch(View v, MotionEvent event){ switch (event.getAction()){ case MotionEvent.ACTION_DOWN: /** * Get the x,y coordinates pressed */ setTouchX(event.getRawX()); setTouchY(event.getRawY()); setDownTime(System.currentTimeMillis()); create(v); break; case MotionEvent.ACTION_CANCEL: .... break; case MotionEvent.ACTION_UP: .... break; } return this; }
Notice that you get the X of an Item listening event on the screen. The y coordinate is getRawX () & getRawY ().
Now that we have got the coordinates we pressed, we can calculate the position of the pop-up menu. The strategy of icon appearance in Pinterest is not only that icons appear around the finger, but also that the direction of icons is determined according to the X and y of the coordinates pressed. That is, when the finger is on the top of the screen, the icon is down, and when the finger is on the right side of the screen, the icon is left. Yes, that is.
- 1. Divide the screen into four areas, determine the area by x,y coordinates, and determine the direction of the icon.
- 2. According to the direction of the icon and the coordinates of X and y, the coordinates of the central point of the icon can be calculated based on the distance angle of 45 degrees between each icon. Here we can use either quadratic function or trigonometric function. I use trigonometric function here.
Step 1: Get the quadrant area by pressing the x,y coordinates on the screen (which is not in the same order as the mathematical quadrant).
/** * Press the x,y coordinates on the screen * getScreenWidth() Screen width * getScreenHeight() Screen Height * @param x x coordinate * @param y y coordinate * @return */ private int getQuadrant(int x, int y){ /** * First region */ if(x < getScreenWidth() / 2 && y < getScreenHeight() / 2){ return 1; } /** * Second region */ if(x < getScreenWidth() && x > getScreenWidth() / 2 && y < getScreenHeight() / 2){ return 2; } /** * Third region */ if(x < getScreenWidth() && x > getScreenWidth() / 2 && y > getScreenHeight() / 2 && y < getScreenHeight()){ return 3; } /** * Region IV */ if(x < getScreenWidth() / 2 && y > getScreenHeight() / 2 && y < getScreenHeight()){ return 4; } return 3; }
When we get the quadrant area, we can know the position of icon and the angle formed by pressing the point. Through the angle and the length of the distance, we can calculate the X and Y coordinates of icon. Then we can make icon appear in the designated position by setting the Layout Params of icon (the relevant code below is not in the same class).
/** * The right-angled edge width of the icon position and the x-axis of the triangle formed by pressing the down point and the x-axis */ protected int mWidth; /** * The icon position and the right-angled edge height where y forms a triangle by pressing the down point and the x-axis */ protected int mHeight; /** * The top left corner x where the icon is located */ protected int mIndexX; /** * The top left corner y where the icon is located */ protected int mIndexY; public void initParams(){ setVisibility(View.GONE); mBaseDegree = 0; setItemId(mITouchView.getItemId()); mNormalResId = mITouchView.getImageResNormal(); mPressResId = mITouchView.getImageResPress(); mTitle = mITouchView.getImageTitle(); mIndex = mITouchView.getImageIndex(); setImageResource(getNormalResId()); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(getIconWidth(),getIconHeight()); /** * high */ mHeight = Math.abs((int)(Math.sin(getAngle(getAngle())) * getR())); /** * wide */ mWidth = Math.abs((int)(Math.cos(getAngle(getAngle())) * getR())); mIndexX = getTouchX() - mWidth - getIconWidth() / 2; mIndexY = getTouchY() - mHeight - getIconHeight() / 2 - getStatusBarHeight(); params.setMargins(getIndexX(), getIndexY(), 0, 0); setLayoutParams(params); } /** * Determine the angle of the quadrant region according to the index order * getFactor() The angle between the icon and the coordinate line is 45 degrees here. * getSingleAngle()The final return is the basic angle calculated from the quadrant. * @return */ public int getAngle(){ return getIndex() * getFactor() + getSingleAngle(); } /** * Calculate the true angle according to the angle of input (for trigonometric function calculation in Math) * @param angle * @return */ public double getAngle(int angle){ return angle * Math.PI / 180; } /** * Angle of two icons * @return */ public int getSingleAngle(){ return getBaseDegree(); } public int getBaseDegree() { switch (getQuadrant(getTouchX(), getTouchY())){ case 1: return mBaseDegree + 135; case 2: return mBaseDegree + 225; case 3: return mBaseDegree + 0; case 4: return mBaseDegree + 45; } int degree = 180 - (1 - getQuadrant(getTouchX(), getTouchY())) * 90; degree = degree > 360 ? degree - 360 : degree; return degree + mBaseDegree; }
Knowing the x,y coordinates of the icon and the angle of the icon, we can now start making an animation that appears and disappears.
public void show(){ setVisibility(View.VISIBLE); invalidate(); TranslateAnimation animation = new TranslateAnimation(getShowAndHideXY()[0], 0, getShowAndHideXY()[1], 0); animation.setDuration(200); animation.setStartOffset(TouchToSelecttorConfig.ANIMATION_START_OFFSET); animation.setInterpolator(new LinearOutSlowInInterpolator()); startAnimation(animation); } public void hide(){ TranslateAnimation animation = new TranslateAnimation(0, getShowAndHideXY()[0], 0, getShowAndHideXY()[1]); animation.setDuration(200); animation.setInterpolator(new LinearOutSlowInInterpolator()); animation.setFillAfter(true); startAnimation(animation); } /** * getW() The right-angled edge width of the icon position and the x-axis of the triangle formed by pressing the down point and the x-axis * getH() The icon position and the right-angled edge height where y forms a triangle by pressing the down point and the x-axis * fromX and fromY for Mobile Animation when Pressed * @return */ private int[] getShowAndHideXY(){ int[] showAndHideXY = new int[2]; switch (getQuadrant(getTouchX(), getTouchY())){ case 1: showAndHideXY[0] = getW() * -1; showAndHideXY[1] = getH() * -1; break; case 2: showAndHideXY[0] = getW(); showAndHideXY[1] = getH() * -1; break; case 3: showAndHideXY[0] = getW(); showAndHideXY[1] = getH(); break; case 4: showAndHideXY[0] = getW() * -1; showAndHideXY[1] = getH(); break; } return showAndHideXY; }
The location of the event and the disappearance of the animation are all solved, so here's how to diffuse the touch event to the entire screen after setting up OnTouchListener in the Adapter Item view.
We just set up an OnTouchListener for Adapter Item view
getHolder(holder).mImage.setOnTouchListener(listener);
But this monitoring area is only a large area of view, and the pop-up window may be either Windows Manager or Dialog, that is, it is not at the same view level as Activity. So how can we make the pop-up view receive our onTouch event?
We can't distribute the onTouch event of view to Windows Manager/Dialog, but we can distribute the event to the Content of Activity or ScrollView, a sub-container of the current Fragment. The pop-up view only needs to touch the X and Y coordinates. We just pass the X and Y coordinates received after distribution to the pop-up view.
This involves the event distribution mechanism of View, such as the nesting of the following containers at the interface level: Activity Container A, Fragment Container B, ScrollView Container C, Recyclerview Container D, Adapter View E. If you know Android's event distribution mechanism, you will know that the order of event distribution is A-> B-> C-> D-> E, if there is a container in the middle. If you rewrite onTouchEvent or set onTouchListener and set the return value to true, the current level ViewGroup will consume the event and will not distribute it downward. Of course, if you set onTouchEvent to return true to intercept the event and block the event downward distribution, then there is no need for parent container to intercept the event here.
Because it's not a sliding conflict itself, we just need to get a larger parent container of the current View's operating area and set it an onTouchListener listener with a true return value to consume the event and not distribute it downwards. That's the point.
So what do we do?
1. Because we only pop up the menu after a long press, that is to say, the requirement should be to pop up the menu before starting to set up onTouchListener for the parent container. This is done by ACTION_DOWN in the onTouch method of the Adapter view. How to deal with it? We can turn on a timer, which waits a long time for us to press, and when it's time, we can start the next operation.
public TouchToSelector onTouch(View v, MotionEvent event){ switch (event.getAction()){ case MotionEvent.ACTION_DOWN: /** * Get the x,y coordinates pressed */ setTouchX(event.getRawX()); setTouchY(event.getRawY()); /** * Record Press Time */ setDownTime(System.currentTimeMillis()); create(v); break; case MotionEvent.ACTION_CANCEL: ... break; case MotionEvent.ACTION_UP: .... break; } return this; } private TouchToSelector create(View view){ this.mView = view; this.mContext = view.getContext(); setCanLongClick(true); initTimer(); return this; } private void initTimer(){ mTimer = new Timer(); TimerTask task = new TimerTask() { @Override public void run() { if(isCanLongClick()){ mHandler.sendEmptyMessage(LONG_CLICK); } } }; mTimer.schedule(task, TouchToSelecttorConfig.LONG_CLICK); }
This is the implementation and process of long-time, so after we receive the message of timer arrival, we should get the parent container and set up onTouchListener, consume the event and do not distribute it downwards, so the handleMessage method does not write.
private void handleOnLongClick(){ dispatchTouchEvent(); initWindowManager(); initBg(); dispatchEvent(); if(null != onLongClickListener){ onLongClickListener.onLongClick(mView); } } private void dispatchTouchEvent(){ getScrollView().setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if(null != mBg){ mBg.onTouchEvent(event); } switch (event.getAction()){ case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: return getScrollView().onTouchEvent(event); } return true; } }); } private ViewGroup getScrollView(){ return (ViewGroup) findViewById(mScrollViewId != 0 ? mScrollViewId : android.R.id.content); }
Notice that I passed the event to mBg in onTouch listening. What is mBg? It's the parent container of our pop-up menu. We just need to make coordinate judgment in mBg to know which icon we touched. The following code excerpts mBg. onTouchEvent () - > ACTION_MOVE and the method inside the icon ImageView to judge by distance and angle. Which icon is the response selected, and the selected icon returns the result by listening
public void handleActionMove(MotionEvent event){ for(TouchToSelectorImageView touchToSelectorImageView:mTouchToSelectorImageViews){ touchToSelectorImageView.handleActionMove(event); } }
public void handleActionMove(MotionEvent event){ if(!isInDistance(event)){ if(mScaleToBig){ hideTitle(); handleItemUnSelect(); } } if(isInDistance(event) && isInAngle(event)){ scaleToBig(); }else { scaleToSmall(); } } private boolean isInDistance(MotionEvent event){ int distance = (int)getTwoPointDistance((int)event.getRawX(), (int)event.getRawY(), getTouchX(), getTouchY() + getStatusBarHeight()); return distance > getR() / 3 && distance < (getR() + getR() / 3); } /** * Obtain the angle between the line formed by the point pressed and the moving point and the X-axis. * @return */ private boolean isInAngle(MotionEvent event){ int x1 = (int)event.getRawX(); int y1 = (int)event.getRawY(); int degree = getTwoPointDegreeDiffTouchXY(x1, y1); int minDegree = getMinDegree(); int maxDegree = getMaxDegree(); return degree > minDegree && degree < maxDegree; }
Now that touching and judging whether the icon is selected has also been solved, there is still a problem, that is, how the peripheral whitening is achieved. Let's look back at the picture below.
Apart from ITEM's Layout, all the other areas are white. How to achieve this is not difficult.
- 1. Set the background of the pop-up View container to be transparent
- 2. Add a View to the background, which is cut into an area, that is, "Select the area of item"
- 3. How to cut it? Path is used here. Path is used to draw the path, and then the View is painted, that is, in the draw method.
canvas.drawPath(path, paint);
How to draw this path? There are many ways to draw paths. Ultimately, you just need to separate the item area. Here, I paste the code of the path I painted, and I don't describe it.
float indexY = y + (dialogMode ? -BaseUtils.getStatusBarHeight() : 0); float itemMeasureWidth = itemLayout.getMeasuredWidth(); float itemMeasureHeight = itemLayout.getMeasuredHeight(); Paint paint=new Paint(); /** * Sawtooth removal */ paint.setAntiAlias(true); paint.setStyle(Paint.Style.FILL); /** * Set the color of paint */ paint.setColor(Color.parseColor(color)); Path path = new Path(); path.moveTo(0, 0); path.lineTo(getScreenWidth(), 0); path.lineTo(getScreenWidth(), indexY); path.lineTo(x, indexY); path.lineTo(x, indexY + itemMeasureHeight); path.lineTo(x + itemMeasureWidth, indexY + itemMeasureHeight); path.lineTo(x + itemMeasureWidth, indexY); path.lineTo(getScreenWidth(), indexY); path.lineTo(getScreenWidth(), getScreenHeight()); path.lineTo(0, getScreenHeight()); path.close(); canvas.drawPath(path, paint); super.draw(canvas);
Note that indexY distinguishes between Dialog mode and Windows Manager mode, and the height of status_bar needs to be subtracted if the status bar exists.
These are the basic principles and ideas of high imitation Pinterest interaction.