屁话不多说,先上个效果图先
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开发艺术探索》,这书还是挺不错的,这里解决点击事件冲突的也是在此书中看来的...
网友评论