先上效果圖
一、需求分析
實現類似美妝相機中高級美妝素材列表。
功能要求如下:
橫向列表,可以左右滑動。提供粘性頭部,點擊頭部進入另外一個Activity,展示所有喜歡的素材。
長按列表中的某個素材,若素材不是喜歡的素材。則將素材標記為喜歡的素材,界面顯示出光環、愛心的效果。
長按列表中的某個素材,若素材已經是喜歡的素材。則可以將素材拖拽出來。當拖拽完成之后,如果拖拽控件和取消喜歡的區域有交集的時候取消喜歡素材。并且有愛心彈出的效果。
拖拉的控件回彈到列表中的時候要求有漸變的動畫效果。
思路分析
首先對于第一點,列表展示。很容易想到的是RecyclerView。而RecyclerView沒有對每個Item提供點擊事件、長按事件的支持。比較簡單的做法就是在適配器中定義回調接口,讓Activity去實現,從而能夠對item的事件進行監聽。數據存儲可以用greenDao將數據存儲到Sqlite數據庫中。最后就是動畫效果了,動畫效果采取的是屬性動畫。
二、具體實現
RecyclerView列表展示
RecyclerView的基本使用有三個點:
設置LayoutManager,用來設置RecyclerView將以什么方式呈現給用戶。比如線性的、網格狀的等等。在這個Demo中用到的是LinearLayoutManager并且設置為橫向的。
添加ItemDecoration主要是用來控制每個item的偏移量。以及可以給item與item之間畫上分割線。
設置Adapter,提供數據源。
/**
* 初始化RecyclerView列表
*/
private void initRecyclerView() {
//設置RecyclerView為橫向列表
mRvEffectList.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
//添加RecyclerView每個item之間的距離
mRvEffectList.addItemDecoration(new EffectItemDecoration());
//從數據庫中查詢數據
mDataList = effectBeanDao.queryBuilder().list();
mAdapter = new EffectAdapter(mDataList, this);
//設置適配器提供數據
mRvEffectList.setAdapter(mAdapter);
}
粘性頭部的實現
其實是在Activity的布局中,在RecyclerView的上層覆蓋了一個View。并且監聽RecyclerView的滑動事件。當RecyclerView的滑動超過一定的距離將上層的View顯示出來,就可以達到RecyclerView有一個粘性固定頭部的效果。
//添加RecyclerView的滑動監聽,當滑動超過item的一半長度的時候顯示粘性頭部
mRvEffectList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mTotalDx += dx;
if (mTotalDx > mHeaderView.getWidth() / 2) {
mTvStickyHeader.setVisibility(View.VISIBLE);
} else {
mTvStickyHeader.setVisibility(View.INVISIBLE);
}
}
});
布局文件
<FrameLayout
android:id="@+id/frame_layout"
android:layout_width="match_parent"
android:layout_height="84dp"
android:layout_alignParentBottom="true">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_effect_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_sticky_header"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:background="#fff"
android:text="我喜歡\n的素材"
android:visibility="invisible" />
</FrameLayout>
為RecyclerView的每個item添加點擊、長按事件
RecyclerView并沒有像ListView那樣提供OnItemClickListener之類的接口。網上有些做法是采取手勢去做每個子Item的事件,例如這篇博客RecyclerView添加onItemClickListener更佳的解決方案。我采取比較簡單的做法,就是讓適配器實現了OnClickListener、OnLongClickListener。調用onCreateViewHolder方法的時候就為每個itemView設置長按事件、點擊事件。提供回調接口讓Activity去實現,在onClick、onLongClick調用回調接口提供的方法。
核心代碼如下:
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_effect, parent, false);
//為每個item設置點擊事件、長按事件監聽器
view.setOnClickListener(this);
view.setOnLongClickListener(this);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
定義回調接口
public interface OnRecyclerViewItemLongClickListener {
void onItemLongClick(View view);
}
public interface OnRecyclerViewItemClickListener {
void onItemClick(View view);
}
@Override
public void onClick(View v) {
if (mOnItemClickListener != null) {
mOnItemClickListener.onItemClick(v);
}
}
@Override
public boolean onLongClick(View v) {
if (mOnItemLongClickListener != null) {
mOnItemLongClickListener.onItemLongClick(v);
}
return true;
}
將RecyclerView中某個素材進行拖拽的實現
實現的思路并不是真的將RecyclerView的item拖出來。而是在Window中添加一個透明的布局,該布局用來承載一個拖拽的FrameLayout,在開始拖拽的時候將承載布局添加到window中,將FrameLayout添加到承載布局中,讓RecyclerView中的item不可見。讓拖拽的FrameLayout跟隨手指移動,從而就可以實現將RecyclerView中的item拖拽出來的效果。拖拽功能的思想可以參考Android自定義Layout實現圖片交換這篇博文。停止拖拽的時候,將FrameLayout移動到原來item的位置,并且將RecyclerView的item設置為可見,就能實現功能需求要求的拖拽回彈的要求了。
private void startDrag(Bitmap bm, float x, float y) {
downX = x;
downY = y;
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mContainer.addView(mDragLayout, lp);
mDragLayout.setVisibility(View.INVISIBLE);
ImageView dragImageView = (ImageView) mDragLayout.findViewById(R.id.iv_drag_effect);
dragImageView.setImageBitmap(bm);
}
//讓拖拽的FrameLayout跟隨手指移動
private void onDrag(float rawX, float rawY) {
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mDragLayout.getLayoutParams();
if (mDragLayout != null) {
mDragLayout.setVisibility(View.VISIBLE);
mDragItemView.setVisibility(View.INVISIBLE);
lp.leftMargin = (int) (rawX - mDragLayoutWidth / 2);
lp.topMargin = (int) (rawY - mDragLayoutHeight / 2);
}
mDragLayout.setLayoutParams(lp);
}
停止拖拽的時候,判斷拖拽的FrameLayout的邊界是否和取消喜歡的邊界有交集。如果有交集,更新數據庫的數據。
/**
* 停止拖拽
* 當取消區域矩形和拖拽的控件的矩形區域有交集的時候,取消喜歡
* @param x
* @param y
*/
private void stopDrag(float x, float y) {
mDragLayoutBound.set(mDragLayout.getLeft(), mDragLayout.getTop(), mDragLayout.getRight(), mDragLayout.getBottom());
if (mDragLayoutBound.intersect(mCancelLikeBound) && mDragBean != null) {
showCancelLikeAnimation(x, y);
} else {
showBackAnimation(x, y);
}
}
動畫效果的實現
需求中有三個動畫效果的要求,分別是
- 長按的時候,若素材沒有被標記為喜歡。則顯示光環、愛心的動畫效果。
- 當拖拽停止的時候,拖拽的item和取消喜歡區域有相交集的時候,將右下角的愛心彈到取消喜歡區域的中心。
- 全屏拖拽控件的時候,在任何位置松開手指,拖拽的item要漸進的回到原本item的位置。
光環以及愛心
光環和愛心是在activity的布局中放置了兩個ImageView長按的時候使用ObjectAnimator進行縮放動畫和透明度動畫。
/**
* 顯示愛心以及光環的動畫
*/
private void showLikeAnimation() {
//動畫監聽器,開始時設置控件可見,結束時設置控件不可見。
AnimatorListenerAdapter likeAnimatorListenerAdapter = new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mIvLove.setVisibility(View.VISIBLE);
mIvAura.setVisibility(View.VISIBLE);
mIvLove.setAlpha(1f);
}
@Override
public void onAnimationEnd(Animator animation) {
mIvAura.setVisibility(View.GONE);
}
};
//光環縮放動畫
ObjectAnimator auraScaleX = ObjectAnimator.ofFloat(mIvAura, "scaleX", 0f, 1f);
ObjectAnimator auraScaleY = ObjectAnimator.ofFloat(mIvAura, "scaleY", 0f, 1f);
AnimatorSet auraAnimatorSet = new AnimatorSet();
//設置動畫時間
auraAnimatorSet.setDuration(1000);
//x軸縮放和y軸縮放同時進行
auraAnimatorSet.play(auraScaleX).with(auraScaleY);
//添加監聽器,監聽動畫開始和結束
auraAnimatorSet.addListener(likeAnimatorListenerAdapter);
auraAnimatorSet.cancel();
auraAnimatorSet.start();
//愛心縮放動畫以及透明度動畫
ObjectAnimator loveScaleX = ObjectAnimator.ofFloat(mIvLove, "scaleX", 0f, 1f);
ObjectAnimator loveScaleY = ObjectAnimator.ofFloat(mIvLove, "scaleY", 0f, 1f);
ObjectAnimator alpha = ObjectAnimator.ofFloat(mIvLove, "alpha", 1f, 0f);
AnimatorSet loveAnimatorSet = new AnimatorSet();
loveAnimatorSet.setDuration(1000);
//先進行縮放再進行透明度播放
loveAnimatorSet.play(loveScaleX).with(loveScaleY).before(alpha);
loveAnimatorSet.addListener(likeAnimatorListenerAdapter);
loveAnimatorSet.cancel();
loveAnimatorSet.start();
}
item回彈動畫
當觸發長按拖拽的時候可以記錄(x1,y1)坐標,當手指抬起的時候又可以獲得另外一個(x2,y2)坐標。使用ValueAnimator并設置AnimatorUpdateListener,在onAnimationUpdate中可以動態改變值,使得從(x2,y2)到(x1,y1)有一個漸變的效果。
/**
* 將拖動控件進行回彈回彈到RecyclerView原本item的位置
*
* @param x 回彈時x軸坐標
* @param y 回彈時y軸坐標
*/
private void showBackAnimation(final float x, final float y) {
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.removeAllUpdateListeners();
animator.removeAllListeners();
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
float currentX = x + (downX - x) * animatedValue;
float currentY = y + (downY - y) * animatedValue;
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mDragLayout.getLayoutParams();
lp.leftMargin = (int) (currentX - mDragLayoutWidth / 2);
lp.topMargin = (int) (currentY - mDragLayoutHeight / 2);
mDragLayout.setLayoutParams(lp);
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mDragItemView.setVisibility(View.VISIBLE);
mContainer.removeView(mDragLayout);
mWindowManager.removeView(mContainer);
mRlCancelLike.setVisibility(View.INVISIBLE);
}
});
animator.setDuration(200);
animator.cancel();
animator.start();
}
取消喜歡愛心消失動畫
愛心的消失采取的是在承載容器中在添加一個ImageView。這個ImageView的初始位置為拖拽FrameLayout的右下角,結束的位置是取消喜歡區域的中心。同上獲得了兩個坐標點之后就可以在監聽器中動態的改變愛心的坐標。
/**
* 取消喜歡動畫:將DragLayout彈回原來RecyclerView中item對應的位置
* 將愛心圖片從DragLayout右下角彈到取消喜歡區域的中心
*
* @param x 手指抬起的時候x坐標
* @param y 手指抬起的時候y坐標
*/
private void showCancelLikeAnimation(final float x, final float y) {
//將愛心移動到取消喜歡區域的中點位置
final float fromX = mDragLayout.getRight();
final float fromY = mDragLayout.getBottom();
final float dstX = mCancelLikeBound.centerX();
final float dstY = mCancelLikeBound.centerY();
//設置DragLayout右下角的愛心不可見
mDragLayout.findViewById(R.id.iv_drag_like).setVisibility(View.INVISIBLE);
final ImageView mIvDragLike = new ImageView(this);
mIvDragLike.setImageResource(R.drawable.like);
final int width = getResources().getDimensionPixelSize(R.dimen.like_width);
final int height = getResources().getDimensionPixelSize(R.dimen.like_height);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(width, height);
lp.leftMargin = (int) (fromX - width / 2);
lp.topMargin = (int) (fromY - height / 2);
mContainer.addView(mIvDragLike);
mIvDragLike.setLayoutParams(lp);
ValueAnimator animator2 = ValueAnimator.ofFloat(0f, 1f);
animator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//根據起始和結束時的坐標設置LayoutParams
float animatedValued = (float) animation.getAnimatedValue();
float currentX = fromX + (dstX - fromX) * animatedValued;
float currentY = fromY + (dstY - fromY) * animatedValued;
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mIvDragLike.getLayoutParams();
lp.leftMargin = (int) (currentX - width / 2);
lp.topMargin = (int) (currentY - height / 2);
mIvDragLike.setLayoutParams(lp);
}
});
animator2.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//動畫結束,設置item右下角的愛心不可見
mDragItemView.findViewById(R.id.iv_like).setVisibility(View.INVISIBLE);
//讓原本隱藏的RecyclerView的item重新可見
mDragItemView.setVisibility(View.VISIBLE);
//移除DragLayout和愛心的ImageView
mContainer.removeView(mDragLayout);
mContainer.removeView(mIvDragLike);
//windows移除承載的容器
mWindowManager.removeView(mContainer);
//設置取消喜歡區域不可見
mRlCancelLike.setVisibility(View.INVISIBLE);
//恢復DragLayout右下角的愛心可見
mDragLayout.findViewById(R.id.iv_drag_like).setVisibility(View.VISIBLE);
//更新數據庫數據
mDragBean.setIsLike(false);
effectBeanDao.update(mDragBean);
}
});
animatorSet.setDuration(200);
animatorSet.play(animator1).with(animator2);
animatorSet.cancel();
animatorSet.start();
}
三、總結
本次Demo主要是學習了RecyclerView的基本使用、怎么對RecyclerView的item設置監聽。簡單了解GreenDao操作數據庫。對動畫的使用加深理解。