屁話不多說辆苔,先上個效果圖先
將此控件放到RecyclerView中算灸,并自定義LayoutManager可以有這樣的效果
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>
代碼中進行操作
控件中已有默認的配合參數(shù),所以可以直接使用,不進行配置
yCardLayout = findViewById(R.id.fl);
//yCardLayout.setMaxWidth(yCardLayout.getWidth());//設(shè)置最大移動距離
//yCardLayout.setMoveRotation(45);//最大旋轉(zhuǎn)角度
//yCardLayout.reset();//重置數(shù)據(jù)
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();
}
});
實現(xiàn)步驟
自定義控件繼承于Framelayout及初始化
public class YCardLayout extends FrameLayout {
public void init(Context context){
setClickable(true);
setEnabled(true);
minLength = ViewConfiguration.get(context).getScaledTouchSlop();//獲取設(shè)備最小滑動距離
post(new Runnable() {
@Override
public void run() {
maxWidth = getWidth();//默認移動最大距離為控件的寬度,這里的參數(shù)用于旋轉(zhuǎn)角度的變化做參照
firstPoint = new Point((int) getX(),(int)getY());//獲取初始位置
isInit = true;
}
});
}
}
實現(xiàn)移動的動畫,還用移動時的旋轉(zhuǎn)
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isRemove && moveAble && isInit && !isRunAnim) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//獲取點擊時的數(shù)據(jù),并存起來
cacheX = event.getRawX();
cacheY = event.getRawY();
downX = event.getRawX();
downY = event.getRawY();
if (firstPoint == null) {//這個正常情況不會執(zhí)行,在這里只是以防萬一
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);//控制控件的旋轉(zhuǎn)
if (onYCardMoveListener != null) {
onYCardMoveListener.onMove(this, moveProgress);//觸發(fā)移動的監(jiān)聽器
}
}
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)) {//移動結(jié)束事件
return true;
}
}
animToReBack(this, firstPoint);//復(fù)位
return true;
}
break;
}
}
return false;
}
加入移動后的復(fù)位動畫
上面的代碼調(diào)用了animToReBack(this, firstPoint);來進行復(fù)位
/**
* 復(fù)位動畫
* @param view
* @param point 復(fù)位的位置
*/
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();//開始復(fù)位動畫
}
控件里的所有動畫都通過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;
}
到這里,控件就可以移動和復(fù)位了,到了刪除動畫的實現(xiàn)了
刪除動畫
刪除動畫有左邊的右邊刪除,刪除的移動軌跡,需要與滑動方向相關(guān),這樣看起來的效果才比較好
這里寫了兩個方法,供刪除時調(diào)用
/**
* 向左移除控件
* @param removeAnimListener
*/
public void removeToLeft(RemoveAnimListener removeAnimListener){
remove(true,removeAnimListener);
}
/**
* 向右移除控件
* @param removeAnimListener
*/
public void removeToRight(RemoveAnimListener removeAnimListener){
remove(false,removeAnimListener);
}
其中remove方法實現(xiàn)為
/**
* 移除控件并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();
}
在動畫開始/結(jié)束/取消懂提供了回調(diào),當然不需要時傳入null就行了
其中調(diào)用計算終點坐標的方法,這個不好解釋,看看計算過程,詳細的就不說了
/**
* 計算移除動畫終點
* @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());
}
而移除的動畫,內(nèi)部其實也是調(diào)用了getAnimToMove(),只是傳入的旋轉(zhuǎn)度為當前的旋轉(zhuǎn)度,且透明度變化結(jié)束為0
到這里控件已經(jīng)可以有移除動畫了,但是會發(fā)現(xiàn)控件內(nèi)的子控件的點擊事件沒有了,所以這里需要解決點擊事件的沖突
解決點擊事件沖突
需要在onInterceptTouchEvent中,對事件進行分發(fā)處理,在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就基本結(jié)束了,接下來就是與RecyclerView的結(jié)合了,結(jié)合之前要加個重置方法,用于重置控件數(shù)據(jù),因為RecyclerView有復(fù)用的功能,不重置會被其他本控件影響
/**
* 重置數(shù)據(jù)
*/
public void reset(){
if (firstPoint != null) {
setX(firstPoint.x);
setY(firstPoint.y);
}
isRemove = false;
moveAble = true;
setRotation(0);
setAlpha(1);
}
結(jié)合RecyclerView
自定義LayoutManager
當然這里的Manager只是做示范作用,實際中可能會出現(xiàn)問題
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進行相應(yīng)的修改.目前使用時,在添加數(shù)據(jù)時需要使用notifyDataSetChanged()來進行刷新,刪除時需要使用notifyItemRemoved(position)和notifyDataSetChanged()一起刷新,不然可能出現(xiàn)問題.
The End
在自定義這個控件中,主要是解決了點擊事件的沖突,移除動畫的終點計算,還有其他的沖突問題,這里的與RecyclerView的結(jié)合使用,其中使用的LayoutManager還有一些問題,將在完善后再加入到GitHub中.最后推薦本書《Android開發(fā)藝術(shù)探索》,這書還是挺不錯的,這里解決點擊事件沖突的也是在此書中看來的...