[大白装逼]自定义YCardLayout

作者: lewis_v | 来源:发表于2018-03-17 18:00 被阅读9次

    屁话不多说,先上个效果图先

    GIF动画录制工具20180317161745.gif

    将此控件放到RecyclerView中,并自定义LayoutManager可以有这样的效果


    GIF动画录制工具20180317162426.gif

    github:https://github.com/lewis-v/YCardLayout

    使用方式

    添加依赖

    Add it in your root build.gradle at the end of repositories:

    
        allprojects {
            repositories {
                ...
                maven { url 'https://jitpack.io' }
            }
        }
    

    Add the dependency

        dependencies {
                compile 'com.github.lewis-v:YCardLayout:1.0.1'
        }
    

    在布局中使用

      <com.lewis_v.ycardlayoutlib.YCardLayout
            android:id="@+id/fl"
            android:layout_marginTop="20dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
            <ImageView
                android:id="@+id/img"
                android:layout_margin="5dp"
                android:src="@mipmap/ic_launcher"
                android:layout_width="200dp"
                android:layout_height="200dp" />
        </com.lewis_v.ycardlayoutlib.YCardLayout>
    

    代码中进行操作

    控件中已有默认的配合参数,所以可以直接使用,不进行配置

    yCardLayout = findViewById(R.id.fl);
            //yCardLayout.setMaxWidth(yCardLayout.getWidth());//设置最大移动距离
            //yCardLayout.setMoveRotation(45);//最大旋转角度
            //yCardLayout.reset();//重置数据
    
            img = findViewById(R.id.img);
            img.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    yCardLayout.removeToLeft(null);
                    Toast.makeText(MainActivity.this,"点击11",Toast.LENGTH_SHORT).show();
                }
            });
    

    实现步骤

    自定义控件继承于Framelayout及初始化

    public class YCardLayout extends FrameLayout {
    public void init(Context context){
            setClickable(true);
            setEnabled(true);
            minLength = ViewConfiguration.get(context).getScaledTouchSlop();//获取设备最小滑动距离
            post(new Runnable() {
                @Override
                public void run() {
                    maxWidth = getWidth();//默认移动最大距离为控件的宽度,这里的参数用于旋转角度的变化做参照
                    firstPoint = new Point((int) getX(),(int)getY());//获取初始位置
                    isInit = true;
                }
            });
        }
    }
    

    实现移动的动画,还用移动时的旋转

     @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (!isRemove && moveAble && isInit && !isRunAnim) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        //获取点击时的数据,并存起来
                        cacheX = event.getRawX();
                        cacheY = event.getRawY();
                        downX = event.getRawX();
                        downY = event.getRawY();
                        if (firstPoint == null) {//这个正常情况不会执行,在这里只是以防万一
                            firstPoint = new Point((int) getX(), (int) getY());
                        }
                        return true;
                    case MotionEvent.ACTION_MOVE:
                        if ((Math.abs(downX-event.getRawX()) > minLength || Math.abs(downY-event.getRawY()) > minLength)) {//只有大于最小滑动距离才算移动了
                            float moveX = event.getRawX();
                            float moveY = event.getRawY();
    
                            if (moveY > 0) {
                                setY(getY() + (moveY - cacheY));//移动Y轴
                            }
                            if (moveX > 0) {
                                setX(getX() + (moveX - cacheX));//移动X轴
                                float moveLen = (moveX - downX) / maxWidth;
                                int moveProgress = (int) ((moveLen) * 100);//移动的距离占整个控件的比例moveProgress%
                                setRotation((moveLen) * 45f);//控制控件的旋转
                                if (onYCardMoveListener != null) {
                                    onYCardMoveListener.onMove(this, moveProgress);//触发移动的监听器
                                }
                            }
                            cacheX = moveX;
                            cacheY = moveY;
                        }
                        return false;
                    case MotionEvent.ACTION_UP:
                        if ((Math.abs(downX-event.getRawX()) > minLength || Math.abs(downY-event.getRawY()) > minLength)) {//移动了才截获这个事件
                            int moveEndProgress = (int) (((event.getRawX() - downX) / maxWidth) * 100);
                            if (onYCardMoveListener != null) {
                                if (onYCardMoveListener.onMoveEnd(this, moveEndProgress)) {//移动结束事件
                                    return true;
                                }
                            }
                            animToReBack(this, firstPoint);//复位
                            return true;
                        }
                        break;
                }
            }
            return false;
        }
    

    加入移动后的复位动画

    上面的代码调用了animToReBack(this, firstPoint);来进行复位

    /**
         * 复位动画
         * @param view
         * @param point 复位的位置
         */
        public void animToReBack(View view,Point point){
            AnimatorSet animatorSet = getAnimToMove(view,point,0,getAlpha());//获取动画
            isRunAnim = true;//动画正在运行的标记
            animatorSet.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {
    
                }
    
                @Override
                public void onAnimationEnd(Animator animation) {
                    isRunAnim = false;
                }
    
                @Override
                public void onAnimationCancel(Animator animation) {
                    isRunAnim = false;
                }
    
                @Override
                public void onAnimationRepeat(Animator animation) {
    
                }
            });
            animatorSet.start();//开始复位动画
        }
    

    控件里的所有动画都通过getAnimToMove来获取,getAnimToMove的代码为

     /**
         * 移动动画
         * @param view
         * @param point
         * @param rotation
         */
        public AnimatorSet getAnimToMove(View view, Point point, float rotation,float alpha){
            ObjectAnimator objectAnimatorX = ObjectAnimator.ofFloat(view,"translationX",point.x);
            ObjectAnimator objectAnimatorY = ObjectAnimator.ofFloat(view,"translationY",point.y);
            ObjectAnimator objectAnimatorR = ObjectAnimator.ofFloat(view,"rotation",rotation);
            ObjectAnimator objectAnimatorA = ObjectAnimator.ofFloat(view,"alpha",alpha);
            AnimatorSet animatorSet = new AnimatorSet();
            animatorSet.playTogether(objectAnimatorR,objectAnimatorX,objectAnimatorY,objectAnimatorA);
            return animatorSet;
        }
    

    到这里,控件就可以移动和复位了,到了删除动画的实现了

    删除动画

    删除动画有左边的右边删除,删除的移动轨迹,需要与滑动方向相关,这样看起来的效果才比较好
    这里写了两个方法,供删除时调用

    /**
         *  向左移除控件
         * @param removeAnimListener
         */
        public void removeToLeft(RemoveAnimListener removeAnimListener){
            remove(true,removeAnimListener);
        }
    
        /**
         * 向右移除控件
         * @param removeAnimListener
         */
        public void removeToRight(RemoveAnimListener removeAnimListener){
            remove(false,removeAnimListener);
        }
    

    其中remove方法实现为

    /**
         * 移除控件并notify
         * @param isLeft 是否是向左
         * @param removeAnimListener
         */
        public void remove(boolean isLeft, final RemoveAnimListener removeAnimListener){
            isRemove = true;
            final Point point = calculateEndPoint(this,this.firstPoint,isLeft);//计算终点坐标
            AnimatorSet animatorSet = getReMoveAnim(this,point,getRemoveRotation(this,this.firstPoint,isLeft));//获取移除动画
            animatorSet.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {
                    if (removeAnimListener != null){
                        removeAnimListener.OnAnimStart(YCardLayout.this);
                    }
                }
    
                @Override
                public void onAnimationEnd(Animator animation) {
                    if (removeAnimListener != null){
                        removeAnimListener.OnAnimEnd(YCardLayout.this);
                    }
                }
    
                @Override
                public void onAnimationCancel(Animator animation) {
                    Log.e("cancel","");
                    reset();
                    if (removeAnimListener != null){
                        removeAnimListener.OnAnimCancel(YCardLayout.this);
                    }
                }
    
                @Override
                public void onAnimationRepeat(Animator animation) {
    
                }
            });
            animatorSet.start();
        }
    

    在动画开始/结束/取消懂提供了回调,当然不需要时传入null就行了
    其中调用计算终点坐标的方法,这个不好解释,看看计算过程,详细的就不说了

     /**
         * 计算移除动画终点
         * @param view
         * @param point
         * @param isLeft
         * @return
         */
        public Point calculateEndPoint(View view, Point point, boolean isLeft){
            Point endPoint = new Point();
            if (isLeft) {
                endPoint.x = point.x - (int) (view.getWidth() * 1.5);
            }else {
                endPoint.x = point.x + (int) (view.getWidth() * 1.5);
            }
             if (Math.abs(view.getX() - point.x) < minLength &&Math.abs (view.getY()-point.y) < minLength){//还在原来位置
                endPoint.y = point.y + (int)(view.getHeight()*1.5);
            }else {
                int endY = getEndY(view,point);
                if (isLeft) {
                    endPoint.y = (int) view.getY() - endY;
                }else {
                    endPoint.y = (int)view.getY() + endY;
                }
            }
            return endPoint;
        }
    
        /**
         * 获取终点Y轴与初始位置Y轴的距离
         * @param view
         * @param point
         * @return
         */
        public int getEndY(View view,Point point){
            return (int) ((point.y-view.getY())/(point.x-view.getX())*1.5*view.getWidth());
        }
    

    而移除的动画,内部其实也是调用了getAnimToMove(),只是传入的旋转度为当前的旋转度,且透明度变化结束为0

    到这里控件已经可以有移除动画了,但是会发现控件内的子控件的点击事件没有了,所以这里需要解决点击事件的冲突

    解决点击事件冲突

    需要在onInterceptTouchEvent中,对事件进行分发处理,在down和up不截获,在move中选择性截获

     @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            boolean intercepted = super.onInterceptTouchEvent(ev);
            if (!isInit || isRunAnim){
                return false;
            }
            switch (ev.getAction()){
                case MotionEvent.ACTION_DOWN:
                    downX = ev.getRawX();
                    downY = ev.getRawY();
                    cacheX = ev.getRawX();
                    cacheY = ev.getRawY();
                    if (firstPoint == null){
                        firstPoint = new Point((int) getX(),(int) getY());
                    }
                    intercepted = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    if ((Math.abs(downX-ev.getRawX()) > minLength || Math.abs(downY-ev.getRawY()) > minLength) && !isRemove && moveAble){
                        intercepted = true;
                    }else {
                        intercepted = false;
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    intercepted = false;
                    break;
            }
            return intercepted;
        }
    

    到这里YCardLayout就基本结束了,接下来就是与RecyclerView的结合了,结合之前要加个重置方法,用于重置控件数据,因为RecyclerView有复用的功能,不重置会被其他本控件影响

     /**
         * 重置数据
         */
        public void reset(){
            if (firstPoint != null) {
                setX(firstPoint.x);
                setY(firstPoint.y);
            }
            isRemove = false;
            moveAble = true;
            setRotation(0);
            setAlpha(1);
        }
    

    结合RecyclerView

    自定义LayoutManager

    当然这里的Manager只是做示范作用,实际中可能会出现问题

    public class YCardLayoutManager extends RecyclerView.LayoutManager {
        public static final String TAG = "YCardLayoutManager";
    
    
        @Override
        public RecyclerView.LayoutParams generateDefaultLayoutParams() {
            return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
                    RecyclerView.LayoutParams.WRAP_CONTENT);
        }
    
    
    
        @Override
        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
            if (getItemCount() == 0) {//没有Item,界面空着吧
                detachAndScrapAttachedViews(recycler);
                return;
            }
            if (getChildCount() == 0 && state.isPreLayout()) {//state.isPreLayout()是支持动画的
                return;
            }
            detachAndScrapAttachedViews(recycler);
            setChildren(recycler);
        }
    
        public void setChildren(RecyclerView.Recycler recycler){
            for (int i = getItemCount()-1; i >= 0; i--) {
                View view = recycler.getViewForPosition(i);
                addView(view);
                measureChildWithMargins(view,0,0);
                calculateItemDecorationsForChild(view,new Rect());
                int width = getDecoratedMeasurementHorizontal(view);
                int height = getDecoratedMeasurementVertical(view);
                layoutDecoratedWithMargins(view,0,0,width,height);
            }
        }
    
        /**
         * 获取某个childView在水平方向所占的空间
         *
         * @param view
         * @return
         */
        public int getDecoratedMeasurementHorizontal(View view) {
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                    view.getLayoutParams();
            return getPaddingRight()+getPaddingLeft()+getDecoratedMeasuredWidth(view) + params.leftMargin
                    + params.rightMargin;
        }
    
        /**
         * 获取某个childView在竖直方向所占的空间
         *
         * @param view
         * @return
         */
        public int getDecoratedMeasurementVertical(View view) {
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                    view.getLayoutParams();
            return getPaddingTop()+getPaddingBottom()+getDecoratedMeasuredHeight(view) + params.topMargin
                    + params.bottomMargin;
        }
    }
    

    然后在RecyclerView中使用YCardLayoutManager加上YCardLayout就能有最开始第二个动图那样的效果,但这里主要是自定义YCardLayout,在与RecyclerView使用的时候还需要对YCardLayoutManager进行相应的修改.目前使用时,在添加数据时需要使用notifyDataSetChanged()来进行刷新,删除时需要使用notifyItemRemoved(position)和notifyDataSetChanged()一起刷新,不然可能出现问题.

    The End

    在自定义这个控件中,主要是解决了点击事件的冲突,移除动画的终点计算,还有其他的冲突问题,这里的与RecyclerView的结合使用,其中使用的LayoutManager还有一些问题,将在完善后再加入到GitHub中.最后推荐本书《Android开发艺术探索》,这书还是挺不错的,这里解决点击事件冲突的也是在此书中看来的...

    相关文章

      网友评论

        本文标题:[大白装逼]自定义YCardLayout

        本文链接:https://www.haomeiwen.com/subject/uqijqftx.html