屁話不多說,先上個效果圖先
將此控件放到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>
代碼中進行操作
控件中已有默認的配合參數,所以可以直接使用,不進行配置
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開發藝術探索》,這書還是挺不錯的,這裏解決點擊事件衝突的也是在此書中看來的…