An attempt to achieve the principle of the Android paging Effect
Respect Original reprinted Please note: From AigeStudio (http://blog.csdn.net/aigestudio) Power by Aige infringement must be investigated!
ArtilleryTown Building
In the first half of the "custom control is actually very simple" series, we used six full sections of nearly 20 thousand words and more than two hundred images to show how to draw Android images, although the length is huge, it is still just the tip of the iceberg for Drawing Images. It aims to get everyone started. As for the achievements of practice, let's look at them ...... In this case, we will try some of the methods we have learned to complete a page flip.
Personally, I do not recommend that you read this article before you try it on your own, because when you read other people's ideas, you will have an inertial mind to lean towards others' ideas, in fact, if you try to analyze it yourself, you may not be able to do it, or even make the method simpler and more efficient. During the analysis, I asked my sister for a new notebook for two or three hours, later, I simulated it in PS and finally got a brief idea through drawing calculation.
Although almost everyone has tried turning pages, unless you have never read a book ...... I have never touched this book ...... I haven't even touched any paper ...... A seemingly simple action actually hides a huge amount of information. If we want to simulate the process of turning pages, it will involve a wide range of things. Here I assume that the page turning effect we have done is similar to that of textbooks, open the paper from the bottom right corner to the left, and our control is assumed to be the right part of the textbook, while the left part is not seen on the left side of our control, the left side of the control can be regarded as the binding line of our textbook. This assumption can simplify the problem. As I said before, the control must be imperfect, if there is a perfect control, we do not need Custom ~ What is the effect of this control? The effect is very simple. Multiple images are input into the control and displayed in turn in the form of pages. The entire page flip principle is figured out. Although I simulate this effect very easily, you can define ViewGroup or ValueAnimation according to my ideas ......
To further simplify the problem, we divide the implementation of the entire page turning effect into four parts. The first part is the attempt Implementation of the page turning, and the second part is the implementation of the line turning, in the third part, we try to introduce a curve to flip the page. In the fourth part, we will introduce some subsequent results and optimize the efficiency. If necessary, we will add some chapters to continue improving the effect. In this way, our process is very clear. In this section, we will first try to flip pages. It would be better if you could use a book or book to follow my ideas, I was planning to take some Photo for demonstration, but I found that page turning is not easy to control. Forget it. Some theoretical things have to be done by yourself and your brain.
First, we need to make some conventions. As mentioned above, the page turning effect we simulate is to set a line on the left side of the control to enable it to be opened from the bottom right corner. This is one of the Conventions. Second, we need to ignore the starting part of the arc relative to this page so that it is always parallel to this page, and again specify that the viewpoint and light source direction are on the top of the paper and the light source is a single light source, the spotlight light cone just covers the paper, it doesn't matter if you don't understand these conventions. I will explain them in detail when it comes to them.
We know that a View is not a container that can hold other controls like a ViewGroup. So how can we add some images to the View in sequence and present them in sequence? Through studying Canvas, we can try to use the "layer" function of Canvas to achieve this effect, in theory, it is feasible to crop Bitmap by using the clipXXX method of Canvas as different "layers" in sequence. What is the actual situation? Let's try to create a new View subclass PageCurlView:
Public class PageTurnView extends View {private List <Bitmap> mBitmaps; // Bitmap data List public PageTurnView (Context context, AttributeSet attrs) {super (context, attrs );}}
Similarly, PageCurlView is related to data. We provide an external method to set data for PageCurlView:
/*** Set Bitmap data ** @ param mBitmaps * Bitmap data List */public synchronized void setBitmaps (List <Bitmap> mBitmaps) {/** if the data is null, an exception is thrown */if (null = mBitmaps | mBitmaps. size () = 0) throw new IllegalArgumentException ("no bitmap to display");/** if the data length is less than 2, GG Smecta */if (mBitmaps. size () <2) throw new IllegalArgumentException ("fuck you and fuck to use imageview"); this. mBitmaps = mBitmaps; invalidate ();}
Note that if there are fewer than two images, there is no need to flip the pages, of course, you can also plot it and then prompt "it is the last page" when the user implements "page turning". Here, I will not allow the number of images to be smaller than two.
In "custom controls are actually very simple 5/12", we have customized a line view. In this example, we set an initialization data for the PolylineView, that is, a group of random values is displayed by default when no data is set. So here I will not initialize the data, but when the data is empty during the painting, we will display a set of text information prompting the user to set the data:
/* ** Display by default ** @ param canvas * Canvas object */private void defaultDisplay (Canvas canvas) {// draw the background canvas. drawColor (Color. WHITE); // draw the title text mTextPaint. setTextSize (mTextSizeLarger); mTextPaint. setColor (Color. RED); canvas. drawText ("fbi warning", mViewWidth/2, mViewHeight/4, mTextPaint); // draw the prompt text mTextPaint. setTextSize (mTextSizeNormal); mTextPaint. setColor (Color. BLACK); canvas. drawText ("Please set data use setBitmaps method", mViewWidth/2, mViewHeight/3, mTextPaint );}
If no data is set, the default display effect of PageCurlView is as follows:
If there is data, we need to adjust the size of the bitmap before drawing the bitmap. Here, I will directly correct the size of the bitmap to the control, of course, in actual application, you can scale the image to maintain the aspect ratio. Here I will discard the aspect ratio directly:
/*** Initialize Bitmap data * scale the Bitmap size to match the screen */private void initBitmaps () {List <Bitmap> temp = new ArrayList <Bitmap> (); for (int I = 0; I <mBitmaps. size (); I ++) {Bitmap bitmap = Bitmap. createScaledBitmap (mBitmaps. get (I), mViewWidth, mViewHeight, true); temp. add (bitmap);} mBitmaps = temp ;}
If the data is ready, we will try to plot it and see:
/*** Plot the bitmap ** @ param canvas * Canvas object */private void drawBtimaps (Canvas canvas) {for (int I = 0; I <mBitmaps. size (); I ++) {canvas. save (); canvas. drawBitmap (mBitmaps. get (I), 0, 0, null); canvas. restore ();}}
As shown in the code above, every time we draw a Bitmap, we lock and restore the Canvas so that each Bitmap is independent during painting for our convenience:
Very spectacular buildings ~ Although we have drawn Bitmap, careful friends will find that the draw order is reversed, and the Bitmap at the end of the list is drawn at the top level, which is very simple, when we are in initBitmaps, isn't it:
private void initBitmaps() {List<Bitmap> temp = new ArrayList<Bitmap>();for (int i = mBitmaps.size() - 1; i >= 0; i--) {Bitmap bitmap = Bitmap.createScaledBitmap(mBitmaps.get(i), mViewWidth, mViewHeight, true);temp.add(bitmap);}mBitmaps = temp;}
At this time, the first image is displayed:
Classical European architecture ~~
Many careful friends may have this question: what is the reverse order in drawBtimaps? There are two reasons: the first is that, since data is initialized, the data we want to obtain after initBitmaps can be used directly, rather than performing calculations without affecting efficiency during draw, the second is that we will execute some calculations in drawBtimaps. Some required parameters include some parameters of the loop. If our loop needs to be calculated independently, it will certainly increase the logic complexity.
The image is drawn, but how can we "hide" the previous image and display the next image at the same time? We can use the clipXXX cropping method mentioned in "custom control is actually very simple 5/12" to display the image by controlling the right coordinate of clipRect. Let's modify the drawBtimaps method and add it to clip:
/*** Plot the bitmap ** @ param canvas * Canvas object */private void drawBtimaps (Canvas canvas) {for (int I = 0; I <mBitmaps. size (); I ++) {canvas. save (); canvas. clipRect (0, 0, mClipX, mViewHeight); canvas. drawBitmap (mBitmaps. get (I), 0, 0, null); canvas. restore ();}}
MClipX is the coordinate value on the right side of the cropping area. We assign it a value in onSizeChanged to make it equal to the control width:
@ Overrideprotected void onSizeChanged (int w, int h, int oldw, int oldh) {// save some code ...... // Initialize the right endpoint coordinate mClipX = mViewWidth ;}
In addition, override the onTouchEvent method of View to obtain the event, and assign the X coordinate of the current touch point to mClipX and redraw the View:
@ Overridepublic boolean onTouchEvent (MotionEvent event) {// obtain the x coordinate of the touch point mClipX = event. getX (); invalidate (); return true ;}
The running effect is as follows:
We can see that although we can crop images by swiping our fingers, the visual test does not achieve our ideal results, clip Crops all the images, but we only want to crop the first image and display it ...... OK, then let's change the drawBtimaps method:
/*** Plot the bitmap ** @ param canvas * Canvas object */private void drawBtimaps (Canvas canvas) {for (int I = 0; I <mBitmaps. size (); I ++) {canvas. save ();/** Crop only the canvas area on the top floor */if (I = mBitmaps. size ()-1) {canvas. clipRect (0, 0, mClipX, mViewHeight);} canvas. drawBitmap (mBitmaps. get (I), 0, 0, null); canvas. restore ();}}
We only crop the canvas area at the top layer, while the others remain unchanged, so that we can get a "flip" effect:
Now, we need to slide from right to left every time, but our fingers have width, it is too troublesome to accurately slide from right to left at a time. We can set an area on both sides of the left and right, when the current touch point is in this area, let our image automatically slide to or adsorption to left or right:
@ Overridepublic boolean onTouchEvent (MotionEvent event) {switch (event. getAction () & MotionEvent. ACTION_MASK) {default: // get the x coordinate of the touch point mClipX = event. getX (); invalidate (); break; case MotionEvent. ACTION_UP: // when the contact is raised, // determines whether to automatically slide judgeSlideAuto (); break;} return true ;}
This event is triggered when the finger is lifted, and the finger is lifted to determine the position of the current vertex:
/*** Determine whether to automatically slide * draw based on the current value of the parameter */private void judgeSlideAuto () {/** if the right endpoint coordinate of the cropping is in the area of 1/5 on the left side of the control, then, we will automatically slide it to the left side of the control */if (mClipX <mViewWidth * 1/5F) {while (mClipX> 0) {mClipX --; invalidate ();}} /** if the right endpoint coordinate of the cropping is in the area of 1/5 on the right side of the control, it is automatically slid to the right side of the control */if (mClipX> mViewWidth * 4/5F) {while (mClipX <mViewWidth) {mClipX ++; invalidate ();}}}
When our touch points are within the Area 1/5 from the left side of the control, the image is automatically absorbed to the left side after the fingers are raised, similarly, when the touch point is within the range of 4/5-5/5 from the right side of the control, the image is automatically absorbed to the right side after the finger is raised:
OK. It seems that there is no problem, right? Hahahahahahahahahahahahahahaif you really want to think so, you will be fooled. You can take a serious look at whether it is really okay. Here I actually dug a hole for you, and it seems like there is no problem, it actually involves a very important piece of information, which we will talk about in the next section. Note that we will continuously trigger the touch event, that is, the onTouchEvent will be continuously called, in onTouchEvent, We will repeatedly calculate the values of mViewWidth * 1/5F and mViewWidth * 4/5F, which is quite unfavorable to the efficiency of our controls, we want to encapsulate it into a member variable and assign the initial value to onSizeChanged:
@ Overrideprotected void onSizeChanged (int w, int h, int oldw, int oldh) {// save some code ...... // The automatically adsorption area on the left and right of the computing control autoAreaLeft = mViewWidth * 1/5F; autoAreaRight = mViewWidth * 4/5F ;}
Call the following in judgeSlideAuto:
Private void judgeSlideAuto () {/** if the right endpoint coordinate of the cropping is in the area of 1/5 on the left side of the control, then, we will automatically slide it to the left side of the control */if (mClipX <autoAreaLeft) {while (mClipX> 0) {mClipX --; invalidate ();}} /** if the right endpoint coordinate of the cropping is in the area of 1/5 on the right side of the control, we will automatically slide it to the right side of the control */if (mClipX> autoAreaRight) {while (mClipX <mViewWidth) {mClipX ++; invalidate ();}}}
The reuse of objects and parameters must be well-developed, especially in methods that may be repeatedly called, such as onDraw and onTouchEvent, avoid unnecessary calculations (especially floating point value calculation) and object generation as much as possible, which plays an important role in improving the View running efficiency!
So far, we have successfully pulled the first image, but how can we continuously flip the remaining image? Before that, let's look at a waste of resources that affect efficiency. I uploaded five images during setBitmaps, that is, the size of the mBitmaps data list is 5, in drawBtimaps, we draw five images in sequence in the Canvas through a loop:
For (int I = 0; I <mBitmaps. size (); I ++) {canvas. save (); // omitting some code ...... Canvas. drawBitmap (mBitmaps. get (I), 0, 0, null); canvas. restore ();}
Now let's see if we have such a need? Here are five images, for example, 10, 100, 1000, and 10000 ...... What about it? In this way, the entire draw will never collapse ~ In fact, we do not need to draw so many images at a time, because we can only display up to two images at a time: the previous one and the next one, that is to say, we only need to display the two pictures at the top level ~ In this way, when there are a large number of images, we can greatly improve the drawing efficiency, and then draw the next two images in the sliding process through some parameter judgment until the last image is drawn, so ~ Let's change the drawBtimaps:
/*** Draw a bitmap ** @ param canvas * Canvas object */private void drawBtimaps (Canvas canvas) {// reset isLastPage to falseisLastPage = false before drawing a bitmap; // The value range of pageIndex is limited. pageIndex = pageIndex <0? 0: pageIndex; pageIndex = pageIndex> mBitmaps. size ()? MBitmaps. size (): pageIndex; // start position of the calculated data int start = mBitmaps. size ()-2-pageIndex; int end = mBitmaps. size ()-pageIndex;/** if the data start point is smaller than 0, it indicates that the current image has reached the last image */if (start <0) {// at This time, set isLastPage to trueisLastPage = true; // and display the prompt showToast ("This is fucking lastest page"); // force reset start to 0; end = 1 ;}for (int I = start; I <end; I ++) {canvas. save ();/** Crop only the canvas area at the top layer * if the last page is reached, no cropping */if (! IsLastPage & I = end-1) {canvas. clipRect (0, 0, mClipX, mViewHeight);} canvas. drawBitmap (mBitmaps. get (I), 0, 0, null); canvas. restore ();}}
We added a member variable pageIndex of the int type to calculate the reference value for reading the data list. Here we agree that the left side of the control is smaller than the autoAreaLeft region as the "rollback Region". What does it mean? If our fingers touch a point in this area, we think that the user's operation is "back to the previous page" (of course, you can also calculate the positive and negative differences between the sliding start points to determine the user's behavior, events are not the focus of this series ). The function of pageIndex can be expressed in simple terms:
Five colors represent five images. The serial number in the upper left corner indicates the subscript position in the list. When pageIndex is 0, start is 3 and end is 5, this means that the final image of the list will be drawn at the top, followed by the penultimate image. If pageIndex is 1, start is 2 and end is 4, this means that the second and last images in the list will be drawn at the top layer, and the last and third images will be drawn ...... And so on, how do we control the value of pageIndex? As we can see from the above, pageIndex ++ indicates that the next page is displayed and pageIndex -- indicates the previous page, so the story is very simple. When our mClipX value is 0, it means that the image has been cropped, at this time, we can make pageIndex ++, and when the user's fingers touch the rollback area, let pageIndex -- display back to the previous page. Since we need to judge the event, so we have to modify onTouchEvent:
@ Overridepublic boolean onTouchEvent (MotionEvent event) {// reset isNextPage to trueisNextPage = true each time the TouchEvent is triggered;/** determine the current event type */switch (event. getAction () & MotionEvent. ACTION_MASK) {case MotionEvent. ACTION_DOWN: // when you touch the screen, // obtain the x coordinate of the current event. mCurPointX = event. getX ();/** if the event point is in the rollback Region */if (mCurPointX <mAutoAreaLeft) {// the next page is not displayed, but the previous page isNextPage = false; pageIndex --; mClipX = mCurPointX; invalidate ();} break; case Mo TionEvent. ACTION_MOVE: // float SlideDis = mCurPointX-event. getX (); if (Math. abs (SlideDis)> mMoveValid) {// obtain the x coordinate of the touch point mClipX = event. getX (); invalidate ();} break; case MotionEvent. ACTION_UP: // when the contact is raised, // determines whether to automatically slide judgeSlideAuto (); /** if the current page is not the last page * if you need to turn over the next page * and the previous page has been clip */if (! IsLastPage & isNextPage & mClipX <= 0) {pageIndex ++; mClipX = mViewWidth; invalidate ();} break;} return true ;}
In the ACTION_MOVE event, the Execution Standard of the event is redefined. If the moving distance is smaller than mMoveValid = mViewWidth * 1/100F, that is, 1% of the control width, it is invalid, the comments of the above Code are the same as those of the above analysis process. The specific effect is as follows:
The following is all the code for this part of PageTurnView. In the next section, we will try to introduce a line to convert the simple switch from right to left into a page flip effect.
Public class PageTurnView extends View {private static final float TEXT_SIZE_NORMAL = 1/40F, TEXT_SIZE_LARGER = 1/20F; // ratio of standard and large text sizes private TextPaint mTextPaint; Paint; // text paint brush private Context mContext; // The Context Environment references private List <Bitmap> mBitmaps; // Bitmap data List private int pageIndex; // The subscript private int mViewWidth and mViewHeight of the currently displayed mBitmaps data; // control width and height private float mTextSizeNormal and mTextSizeLarger; // standard text size and large text size pr Ivate float mClipX; // crop the coordinates of the right endpoint private float mAutoAreaLeft and mAutoAreaRight; // The private float mCurPointX on the left and right sides of the control; // private float mMoveValid of the coordinate value of X when the fingertip touches the screen; // valid distance of the moving event to private boolean isNextPage and isLastPage; // whether to display the next page and whether the value of the last page is public PageTurnView (Context context, AttributeSet attrs) {super (context, attrs); mContext = context; /** instantiate the text Paint brush and set the parameter */mTextPaint = new TextPaint (Paint. ANTI_ALIAS_FLAG | P Aint. DITHER_FLAG | Paint. LINEAR_TEXT_FLAG); mTextPaint. setTextAlign (Paint. align. CENTER) ;}@ Overridepublic boolean onTouchEvent (MotionEvent event) {// reset isNextPage to trueisNextPage = true for each trigger of TouchEvent;/** determine the current event type */switch (event. getAction () & MotionEvent. ACTION_MASK) {case MotionEvent. ACTION_DOWN: // when you touch the screen, // obtain the x coordinate of the current event. mCurPointX = event. getX ();/** if the event point is in the rollback Region */if (mCurPointX <mAutoAreaLeft) {// then Instead of turning over the next page, the previous page isNextPage = false; pageIndex --; mClipX = mCurPointX; invalidate () ;} break; case MotionEvent. ACTION_MOVE: // float SlideDis = mCurPointX-event. getX (); if (Math. abs (SlideDis)> mMoveValid) {// obtain the x coordinate of the touch point mClipX = event. getX (); invalidate ();} break; case MotionEvent. ACTION_UP: // when the contact is raised, // determines whether to automatically slide judgeSlideAuto (); /** if the current page is not the last page * if you need to turn over the next page * and the previous page has been clip */if (! IsLastPage & isNextPage & mClipX <= 0) {pageIndex ++; mClipX = mViewWidth; invalidate ();} break;} return true ;} /*** determine whether automatic slide is required * automatically slide based on the current value of the parameter */private void judgeSlideAuto () {/** if the right endpoint coordinate of the cropping is within 10 of the control's left end, we will directly slide it to the control's left end */if (mClipX <mAutoAreaLeft) {while (mClipX> 0) {mClipX --; invalidate () ;}}/** if the right endpoint coordinate of the cropping is in the area of 10 to the right of the control, then, we will automatically slide it to the right side of the control */if (mClipX> mAutoAreaRight) {while (mClipX <mViewWidth) {MClipX ++; invalidate () ;}}@ Overrideprotected void onSizeChanged (int w, int h, int oldw, int oldh) {// obtain the control width and height mViewWidth = w; mViewHeight = h; // initialize the bitmap data initBitmaps (); // calculate the text size mTextSizeNormal = TEXT_SIZE_NORMAL * mViewHeight; mTextSizeLarger = TEXT_SIZE_LARGER * mViewHeight; // initialize and crop the right endpoint coordinate mClipX = mViewWidth; // calculate the automatically adsorption region on the left and right of the control mAutoAreaLeft = mViewWidth * 1/5F; mAutoAreaRight = mViewWidth * 4; // count Calculate the effective distance from mMoveValid = mViewWidth * 1/100F;}/*** initialize bitmap data * scale the bitmap size to match with the screen */private void initBitmaps () {List <Bitmap> temp = new ArrayList <Bitmap> (); for (int I = mBitmaps. size ()-1; I> = 0; I --) {Bitmap bitmap = Bitmap. createScaledBitmap (mBitmaps. get (I), mViewWidth, mViewHeight, true); temp. add (bitmap);} mBitmaps = temp;} @ Overrideprotected void onDraw (Canvas canvas) {/** if the data is empty, the default prompt text is displayed */if (null = MBitmaps | mBitmaps. size () = 0) {defaultDisplay (canvas); return;} // draw a bitmap drawBtimaps (canvas );} /* ** display by default ** @ param canvas * Canvas object */private void defaultDisplay (Canvas canvas) {// draw the background canvas. drawColor (Color. WHITE); // draw the title text mTextPaint. setTextSize (mTextSizeLarger); mTextPaint. setColor (Color. RED); canvas. drawText ("fbi warning", mViewWidth/2, mViewHeight/4, mTextPaint); // draw the prompt text mTextPaint. setTex TSize (mTextSizeNormal); mTextPaint. setColor (Color. BLACK); canvas. drawText ("Please set data use setBitmaps method", mViewWidth/2, mViewHeight/3, mTextPaint );} /*** draw a bitmap ** @ param canvas * Canvas object */private void drawBtimaps (Canvas canvas) {// reset isLastPage to falseisLastPage = false before drawing a bitmap; // The value range of pageIndex is limited. pageIndex = pageIndex <0? 0: pageIndex; pageIndex = pageIndex> mBitmaps. size ()? MBitmaps. size (): pageIndex; // start position of the calculated data int start = mBitmaps. size ()-2-pageIndex; int end = mBitmaps. size ()-pageIndex;/** if the data start point is smaller than 0, it indicates that the current image has reached the last image */if (start <0) {// at This time, set isLastPage to trueisLastPage = true; // and display the prompt showToast ("This is fucking lastest page"); // force reset start to 0; end = 1 ;}for (int I = start; I <end; I ++) {canvas. save ();/** Crop only the canvas area at the top layer * if the last page is reached, no cropping */if (! IsLastPage & I = end-1) {canvas. clipRect (0, 0, mClipX, mViewHeight);} canvas. drawBitmap (mBitmaps. get (I), 0, 0, null); canvas. restore () ;}/ *** set Bitmap data ** @ param bitmaps * Bitmap data List */public synchronized void setBitmaps (List <Bitmap> bitmaps) {/** if the data is null, an exception is thrown */if (null = bitmaps | bitmaps. size () = 0) throw new IllegalArgumentException ("no bitmap to display");/** if the data length is less than 2, GG Smecta */if (bitmaps. size () <2) throw new IllegalArgumentException ("fuck you and fuck to use imageview"); mBitmaps = bitmaps; invalidate ();} /*** Toast display ** @ param msg * Toast display text */private void showToast (Object msg) {Toast. makeText (mContext, msg. toString (), Toast. LENGTH_SHORT ). show ();}}
Download this part of the source code: Portal