先上效果图
效果图.gif
一、需求分析
实现类似美妆相机中高级美妆素材列表。
功能要求如下:
横向列表,可以左右滑动。提供粘性头部,点击头部进入另外一个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);
}
}
});
布局文件
android:id="@+id/frame_layout"
android:layout_width="match_parent"
android:layout_height="84dp"
android:layout_alignParentBottom="true">
android:id="@+id/rv_effect_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
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" />
为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操作数据库。对动画的使用加深理解。