Android自定义控件安卓开发博客

[大白装逼]自定义YCardLayout

2018-03-17  本文已影响9人  lewis_v

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

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开发艺术探索》,这书还是挺不错的,这里解决点击事件冲突的也是在此书中看来的...

上一篇下一篇

猜你喜欢

热点阅读