Custom viewGroup+ViewDragHelper: simulate the home page card slide, cascading layout
Saw Dalao in the group the other dayZhang XutongUse RecycleView to write a this effect but I am not familiar with custom LayoutManager, just in learning custom view, so I thought of using custom ViewGroup to write try, not to say much, first effect diagram.
The data comes from douban’s movie rating list. As can be seen from the figure, we can slide the topview card on the top layer, and then the cards below will also become larger. Top-1view will become larger to be consistent with TopView. Now top-1View becomes TopView.
In general, it is divided into the following small functions.
-
Drag and drop the top-level view (using the utility class ViewDragHelperI recommend this article by Xiang Ge) and angular rotation
-
Zoom in and out of the following pages
- Slide to a certain point and delete
So let’s go to the code first
public class SwipeCardView extends ViewGroup {
private static final String TAG = "SwipeCardView";
public static int TRANS_Y_GAP;
// The width between the card steps, in px
private int transY = 12;
private ViewDragHelper mDragHelper;
// Top page, swipe with your finger
private View topView;
// Card center point
private int centerX,centerY;
// Finger off screen judgment
private boolean isRelise;
// Load the adapter of the data
private CardBaseAdapter adapter;
// The visible card page
private int showCards = 3;
// Slide the rotation Angle of the card with your finger
private int ROTATION = 20;
// Swipe left and right to judge
private boolean swipeLeft = false;
// Number of pages that have been deleted
private int deleteNum;
// The row width and height of the subview
int childWidth, childHeight;
public SwipeCardView(Context context) {
this(context, null);
}
public SwipeCardView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SwipeCardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TRANS_Y_GAP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, transY, context.getResources().getDisplayMetrics());
mDragHelper = ViewDragHelper.create(this.1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == topView;
}
@Override
public int clampViewPositionHorizontal(View changedView, int left, int dx) {
if (isRelise) {
isRelise = false;
}
for (int i = 1; i < getChildCount()-1; i++) {
View view = getChildAt(i);
view.setTranslationY((childHeight*0.025f+TRANS_Y_GAP) * ( getChildCount()-1- i)
-getCenterX(changedView)*(childHeight*0.025f+TRANS_Y_GAP));
view.setScaleX(1-( getChildCount()-1-i)*0.05f + getCenterX(changedView) * 0.05f);
view.setScaleY(1-( getChildCount()-1-i)*0.05f + getCenterX(changedView) * 0.05f);
}
if(topView! =null) {if (swipeLeft){
topView.setRotation(-getCenterX(changedView) * ROTATION);
}else{ topView.setRotation(getCenterX(changedView) * ROTATION); }}return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// super.onViewReleased(releasedChild, xvel, yvel);
// The mAutoBackView finger can automatically go back when released
if (releasedChild.getLeft() / 2 > 300) {
if (releasedChild == topView) {
removeView(topView);
deleteNum++;
for (int i = 1; i < getChildCount()-1; i++) {
View view = getChildAt(i);
int level = getChildCount()-1-i;
view.setTranslationY((childHeight*0.025f+TRANS_Y_GAP) * (level));
view.setScaleX(1 - 0.05f * ( level));
view.setScaleY(1 - 0.05f * ( level)); } adapter.notifyDataSetChanged(); }}else {
isRelise = true;
mDragHelper.settleCapturedViewAt((int) (centerX-childWidth/2), (int) (centerY-childHeight/2)); invalidate(); }}@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx,
int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
// Move the top card when the finger is released
if (changedView == topView && isRelise) {
for (int i = 1; i < getChildCount()-1; i++) {
View view = getChildAt(i);
int level = getChildCount()-1-i;
view.setTranslationY((childHeight*0.025f+TRANS_Y_GAP) * ( level)-
getCenterX(changedView)*(childHeight*0.025f+TRANS_Y_GAP));
view.setScaleX(1-(level)*0.05f + getCenterX(changedView) * 0.05f);
view.setScaleY(1-(level)*0.05f + getCenterX(changedView) * 0.05f);
}
if(topView! =null) {// Measure the rotation Angle of the card according to the Angle
if (swipeLeft){
topView.setRotation(-getCenterX(changedView) * ROTATION);
}else{ topView.setRotation(getCenterX(changedView) * ROTATION); }}}}}); mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); }private float getCenterX(View child) {
if (child.getWidth() / 2 + child.getX() - centerX<0){
swipeLeft = true;
}else {
swipeLeft = false;
}
float width = Math.abs(child.getWidth() / 2 + child.getX() - centerX);
if (width > centerX) {
width = centerX;
}
return width / centerX;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
centerX = widthSize / 2;
centerY = heightSize/2;
measureChildren( widthMeasureSpec, heightMeasureSpec);
/ / the view
View child = null;
// Get the margin of the child view
MarginLayoutParams params = null;
if (getChildCount()>0){
child = getChildAt(0);
// I just use the size of the first page as the length, because it can't be larger than it
measureChild(child, widthMeasureSpec, heightMeasureSpec);
params = (MarginLayoutParams) child.getLayoutParams();
childWidth = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
childHeight = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) { invalidate(); }}@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
topView = getChildAt(getChildCount()-1);
int level = getChildCount() - 1;
View view;
if (getChildCount() > 1) {
for (int j = 0; j<=getChildCount() -1; j++) {
view = getChildAt(j);
view.layout((int) (centerX-childWidth/2), (int) (centerY-childHeight/2),
(int) (centerX+childWidth/2), (int) (centerY+childHeight/2));
view.setTranslationY((childHeight*0.025f+TRANS_Y_GAP) * (level - 1));
view.setScaleX(1 - 0.05f * (level - 1));
view.setScaleY(1 - 0.05f * (level - 1));
// For clarification, although you can see 4 cards, 5 rows are loaded, and chapter 5 overlaps with chapter 4 in order to slide the top view
// The fourth card can be displayed when the fourth card slides, so the position of the fourth and fifth cards is the same here.
if(j! =0){ level--; }}}else if (getChildCount() > 0) {
view = getChildAt(0);
view.layout((int) (centerX-childWidth/2), (int) (centerY-childHeight/2),
(int) (centerX+childWidth/2), (int) (centerY+childHeight/2)); }}public void setAdapter(@NonNull CardBaseAdapter adapter) {
if (adapter == null) throw new NullPointerException("Adapter cannot be empty");
this.adapter = adapter;
You need to display several pages to initialize the data
changeViews();
adapter.registerDataSetObserver(new DataSetObserver() {
@Override
public void onChanged() {
getMore();
}
@Override
public void onInvalidated() { getMore(); }}); }public void getMore() {
if (getChildCount()+deleteNum<adapter.getCount()){
View view = adapter.getView(getChildCount()+deleteNum,
getChildAt(getChildCount()),this);
// All data is placed at the bottom
addView(view,0); }}private void changeViews() {
View view = null;
/** * showCards-j is the order in which you want to display the cards. ** viewgroup is the first view to be added. AddView (view,j); addView(view,j); showCards =3; addView(view,j); * deleteNum is the number of pages you right-click to delete */
for (int j = 0; j <=showCards; j++) {
if (j+deleteNum<adapter.getCount()){
view = adapter.getView(showCards-j, getChildAt(j),this); addView(view,j); }}}@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
return mDragHelper.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragHelper.processTouchEvent(event);
return true;
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(),attrs);
}
public SwipeCardView setShowCards(int showCards) {
this.showCards = showCards;
return this;
}
public SwipeCardView setTransY(int transY) {
this.transY = transY;
return this; }}Copy the code
Here is the most important onLayout code analysis, other sliding algorithms and this is basically the same
topView = getChildAt(getChildCount()-1);
int level = getChildCount() - 1;
View view;
if (getChildCount() > 1) {
for (int j = 0; j<=getChildCount() -1; j++) {
view = getChildAt(j);
view.layout((int) (centerX-childWidth/2), (int) (centerY-childHeight/2),
(int) (centerX+childWidth/2), (int) (centerY+childHeight/2));
view.setTranslationY((childHeight*0.025 f+TRANS_Y_GAP) * (level - 1));
view.setScaleX(1 - 0.05 f * (level - 1));
view.setScaleY(1 - 0.05 f * (level - 1));
if(j! =0){ level--; }}}else if (getChildCount() > 0) {
view = getChildAt(0);
view.layout((int) (centerX-childWidth/2), (int) (centerY-childHeight/2),
(int) (centerX+childWidth/2), (int) (centerY+childHeight/2));
}Copy the code
- . As shown in the figure, showCards is the number of visible cards, and TRANS_Y_GAP is the width exposed at the bottom. Here is the calculation of the following piece to facilitate the layout below.
- In the code, 0.05F is the scaling ratio. The first layer is scaled 0.05, the second layer is scaled 0.10, the third layer is scaled 0.15, and so on. In the figure above, the two color marked scaling areas are half of 0.05F respectively, which can be seen in the following code.
View. setTranslationY((childHeight*0.025f+TRANS_Y_GAP) * (level-1));
- Once the layout has positioned them, you can move them. ChildHeight *0.025f moves the distance of the color blocks above and then adds TRANS_Y_GAP times their order to complete the layout.
- Behind clampViewPositionHorizontal onViewReleased and onViewPositionChanged method of algorithm and the like. General comments have been written in the code, and do not understand the message I can.
- It’s a good idea to learn about ViewDragHelper first
- The adapter in the article is a customized adapter written by myself. I will not list it here. If you want, you can download it yourself.
- If you are not satisfied with the size of the card, you can set the 0.05F by yourself. I forgot to set it as a global variable here, and I did not add the click event. If you need it, you can add it yourself.
- Here I set up two externally controllable variables, the number of visible cards and the distance between cards, which can be called externally
swipeCards.setShowCards(5)
.setTransY(50)
.setAdapter(new CardBaseAdapter(this,subjectsList));
I haven’t written a blog for a long time. I don’t know how to write or express myself. Temporarily think of so much, and the idea can leave a message to me, I will see, en so much.