转载请注明出处
http://blog.csdn.net/oddshou/article/details/73558553
上一篇文章仿腾讯新闻频道定制界面效果1中在实现过程中有很多不如意的地方,可以说是失败的。但起码是一个好的开始吧。
这两天继续将这个功能完善了一下,效果还算可以,但是还不够完美。最后我会说明不完美的地方。
先上效果图:
对了,这里我只处理了效果层面的,至于数据层应当不是什么难事了。
这里把用到的完整代码贴出来:
/**
* Created by oddshou on 2017/6/19.
*/
public class GridLayoutAnimation extends Activity implements
View.OnClickListener{
private static final String TAG = "GridLayoutAnimation";
LinearLayout rootLayout;
static final String[] CHANNELS_CHOOSED = {"要闻", "视频", "广东", "娱乐", "体育",
"要闻2", "视频2", "广东2", "娱乐2", "体育2",
"要闻3", "视频3", "广东3", "娱乐3", "体育3",
"要闻4", "视频4", "广东4", "娱乐4", "体育4",
};
static final String[] CHANNELS_UNCHOOSED = {"宠物", "纪录片", "文化", "动漫", "股票",
"宠物2", "纪录片2", "文化2", "动漫2", "股票2",
"宠物3", "纪录片3", "文化3", "动漫3", "股票3",
"宠物4", "纪录片4", "文化4", "动漫4", "股票4",
};
private DragGridLayout gridLayoutUnChoosed;
private DragGridLayout gridLayoutChoosed;
private ArrayList<BtnData> groupDataChoosed;
private ArrayList<BtnData> groupDataUnChoosed;
private LayoutTransition mTransitionerTop;
private LayoutTransition mTransitionerBottom;
//内部调换位置
private boolean choosedChange;
private int choosedChangeId;
private View moveView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_container);
rootLayout = (LinearLayout) findViewById(R.id.container);
createPage();
}
protected void createPage() {
//尝试过设置同一个LayoutTransition,发现会有错乱,大概是因为
//动画是两块的,同步执行会有问题。
mTransitionerTop = new LayoutTransition();
mTransitionerBottom = new LayoutTransition();
setupCustomAnimations();
//1.titile 已选频道
TextView chooseTitle = new TextView(this);
chooseTitle.setWidth(200);
chooseTitle.setHeight(100);
chooseTitle.setText("已选频道");
rootLayout.addView(chooseTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
//2.buttons 已选频道
gridLayoutChoosed = new DragGridLayout(this);
gridLayoutChoosed.setChangeItemCallback(callback);
gridLayoutChoosed.setColumnCount(4);
gridLayoutChoosed.setLayoutTransition(mTransitionerTop);
groupDataChoosed = new ArrayList<BtnData>();
createBtns(CHANNELS_CHOOSED, gridLayoutChoosed, groupDataChoosed);
rootLayout.addView(gridLayoutChoosed);
//3.title 推荐频道
TextView unChoosedTitle = new TextView(this);
unChoosedTitle.setWidth(200);
unChoosedTitle.setHeight(100);
unChoosedTitle.setText("未选频道");
rootLayout.addView(unChoosedTitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
//4.buttons 推荐频道
gridLayoutUnChoosed = new DragGridLayout(this);
gridLayoutUnChoosed.setColumnCount(4);
gridLayoutUnChoosed.setLayoutTransition(mTransitionerBottom);
groupDataUnChoosed = new ArrayList<BtnData>();
createBtns(CHANNELS_UNCHOOSED, gridLayoutUnChoosed, groupDataUnChoosed);
rootLayout.addView(gridLayoutUnChoosed);
}
private LayoutTransition.TransitionListener mTransitionListner = new LayoutTransition.TransitionListener() {
int[] locationSrc = new int[2];
@Override
public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
if (transitionType == LayoutTransition.DISAPPEARING) {
view.getLocationInWindow(locationSrc);
} else if (transitionType == LayoutTransition.APPEARING) {
//点击.由上到下 终点为下面第一个。
//由下到上,终点为上面最后一个。
int[] locationDst = new int[2];
if (!choosedChange) {
if (transition == mTransitionerTop) {
//由下到上
getLocation(locationDst);
} else if (transition == mTransitionerBottom) {
//由上到下
View dstView = gridLayoutUnChoosed.getChildAt(0);
dstView.getLocationInWindow(locationDst); //这里获取得到0,0,改用其他方式获取
}else {
return;
}
}else {
View dstView = gridLayoutChoosed.getChildAt(choosedChangeId);
dstView.getLocationInWindow(locationDst); //这里获取得到0,0,改用其他方式获取
}
PropertyValuesHolder pvhTransX =
PropertyValuesHolder.ofFloat("translationX", locationSrc[0] - locationDst[0], 0f);
PropertyValuesHolder pvhTransY =
PropertyValuesHolder.ofFloat("translationY", locationSrc[1] - locationDst[1], 0f);
final ObjectAnimator animIn = ObjectAnimator.ofPropertyValuesHolder(
this, pvhTransX, pvhTransY).
setDuration(transition.getDuration(LayoutTransition.APPEARING));
transition.setAnimator(LayoutTransition.APPEARING, animIn);
animIn.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
choosedChange = false;
}
});
}
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
container.postInvalidate(); //这主要解决转换过程中的一些异常
}
private void getLocation(int[] location) {
//1.计算新增控件行列
int childCount = gridLayoutChoosed.getChildCount();
int columnCount = gridLayoutChoosed.getColumnCount();
int row = (int)Math.ceil(( childCount + 1.0 )/columnCount + 0.5);
int column = (childCount + 1) % columnCount;
if (column > 1) {
View columnBefor = gridLayoutChoosed.getChildAt(childCount - 1);
columnBefor.getLocationInWindow(location);
location[0] += columnBefor.getWidth(); //这里理论上还需要加上一些margin left,right
}else {
//这里认为已选频道大于1
View columnAbove = gridLayoutChoosed.getChildAt(childCount - columnCount);
columnAbove.getLocationInWindow(location);
location[1] += columnAbove.getHeight(); //同样这里也是不准确的
}
}
};
private void setupCustomAnimations() {
//把延时去掉,速度瞬间提升
// mTransitionerTop.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
// mTransitionerTop.setStartDelay(LayoutTransition.APPEARING, 0);
mTransitionerTop.addTransitionListener(mTransitionListner);
mTransitionerTop.setAnimator(LayoutTransition.DISAPPEARING, null);
mTransitionerTop.setAnimator(LayoutTransition.APPEARING, null);
mTransitionerTop.setInterpolator(LayoutTransition.APPEARING, new LinearInterpolator());
mTransitionerTop.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
mTransitionerTop.setStartDelay(LayoutTransition.APPEARING, 0);
// mTransitionerBottom.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
// mTransitionerBottom.setStartDelay(LayoutTransition.APPEARING, 0);
mTransitionerBottom.addTransitionListener(mTransitionListner);
mTransitionerBottom.setAnimator(LayoutTransition.DISAPPEARING, null);
mTransitionerBottom.setAnimator(LayoutTransition.APPEARING, null);
mTransitionerBottom.setInterpolator(LayoutTransition.APPEARING, new LinearInterpolator());
mTransitionerBottom.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
mTransitionerBottom.setStartDelay(LayoutTransition.APPEARING, 0);
}
protected void createBtns(String[] titles, ViewGroup rootView, ArrayList<BtnData> groupList) {
for (int i = 0; i < titles.length; i++) {
Button btn = new Button(this);
// btn.setOnLongClickListener(this);
btn.setOnClickListener(this);
// btn.setOnDragListener(this);
btn.setWidth(160);
btn.setText(titles[i]);
btn.setBackgroundResource(R.drawable.button_selector);
btn.setTextColor(Color.BLACK);
BtnData btnData = new BtnData(i, titles[i], titles == CHANNELS_CHOOSED);
btn.setTag(btnData);
groupList.add(btnData);
GridLayout.LayoutParams lp = new GridLayout.LayoutParams();
lp.setMargins(10, 10, 10, 10);
btn.setLayoutParams(lp);
rootView.addView(btn);
}
}
@Override
public void onClick(View v) {
//已选频道点击移动到未选频道
//未选频道点击移动到已选频道
BtnData tag = (BtnData) v.getTag();
if (tag.choosed) {
gridLayoutChoosed.removeView(v);
//这里如果不添加新的btn 会有问题,原因似乎是原btn有一些坐标属性
//导致添加到新的父控件计算位置有误
gridLayoutUnChoosed.addView(v, 0);
tag.choosed = false;
if (groupDataChoosed.contains(tag)) {
groupDataChoosed.remove(tag);
}
if (!groupDataUnChoosed.contains(tag)) {
groupDataUnChoosed.add(tag);
}
} else {
gridLayoutUnChoosed.removeView(v);
gridLayoutChoosed.addView(v);
tag.choosed = true;
if (!groupDataChoosed.contains(tag)) {
groupDataChoosed.add(tag);
}
if (groupDataUnChoosed.contains(tag)) {
groupDataUnChoosed.remove(tag);
}
}
}
public class BtnData {
int id;
String title;
boolean choosed;
public BtnData(int id, String title, boolean choosed) {
this.id = id;
this.title = title;
this.choosed = choosed;
}
}
private DragGridLayout.ChangeItemCallback callback = new DragGridLayout.ChangeItemCallback() {
@Override
public void changeItem(int indexEnd, View childView) {
if (choosedChange)
return;
int indexStart = gridLayoutChoosed.indexOfChild(childView);
choosedChange = true;
choosedChangeId = indexEnd > indexStart ? indexEnd-1 : indexEnd;
//如果起点小于终点,计算终点坐标时减1,大于终点不减
// View view = gridLayoutChoosed.getChildAt(indexStart);
gridLayoutChoosed.removeView(childView);
gridLayoutChoosed.addView(childView, indexEnd);
// Logger.i(TAG, "changeItem: " + indexStart + " : " + indexEnd, "oddshou");
}
};
}
自定义gradLayout
public class DragGridLayout extends GridLayout {
private static final int scrollSpeed = 20;
private static final String TAG = "DragGridLayout";
/**
* 长按视为拖动图标
*/
private long dragRespondTime = 500;
/**
* 手指按下的点坐标
*/
private int mDownX, mDownY;
/**
* 手指移动的距离
*/
private int moveX, moveY;
/**
* 状态栏高度
*/
private int mStatusHeight = 0;
/**
* 手指按下坐标到屏幕边框的偏移值
*/
private int mOffsetTop, mOffsetLeft;
/**
* 手指按下的position
*/
private int mDragPosition = 0;
/**
* 手指按下的view
*/
private View mStartDragItemView;
/**
* 镜像imageview组件
*/
private ImageView mDragImageView;
WindowManager.LayoutParams winLayoutParams;
/**
* 是否支持拖动界面
*/
private boolean isDrag = false;
/**
* 震动
*/
private Vibrator vibrator;
private WindowManager windowManager;
private static final long HOVER_TIEM = 200;
private static final int LONG_PRESS = 2;
private static final int HOVER =3;
private boolean changing;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case LONG_PRESS:
// 绘图缓存
mStartDragItemView.setDrawingCacheEnabled(true);
// 创建镜像
Bitmap mDragBitmap = Bitmap.createBitmap(mStartDragItemView
.getDrawingCache());
// 释放缓存
mStartDragItemView.destroyDrawingCache();
isDrag = true;
createDragImage(mDragBitmap, mDownX, mDownY);
mStartDragItemView.setBackgroundResource(R.drawable.btn_bg_dashgap);
break;
case HOVER:
if ( !changing) {
// Logger.i(TAG, "dispatchTouchEvent: " + moveX + " : " + moveY, "oddshou");
int index = pointToPosition(moveX, moveY);
int indexStart = indexOfChild(mStartDragItemView);
if (index != -1 && index != indexStart && mCallback != null) {
Logger.i(TAG, "dispatchTouchEvent: " + indexStart + " : " + index, "oddshou");
changing = true;
mCallback.changeItem(index, mStartDragItemView);
mDragPosition = index;
}
changing = false;
}
break;
}
}
};
public DragGridLayout(Context context) {
super(context);
windowManager = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
mStatusHeight = getStatusHeight(context);
}
public void createAnimation() {
PropertyValuesHolder pvhTransX =
PropertyValuesHolder.ofFloat("translationX", -mStartDragItemView.getWidth()/2 + moveX - mStartDragItemView.getLeft(), 0);
PropertyValuesHolder pvhTransY =
PropertyValuesHolder.ofFloat("translationY", -mStartDragItemView.getHeight() +15 + moveY - mStartDragItemView.getTop(), 0);
final ObjectAnimator animIn = ObjectAnimator.ofPropertyValuesHolder(
mStartDragItemView, pvhTransX, pvhTransY).
setDuration(500);
animIn.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
}
});
animIn.start();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = (int) ev.getX();
mDownY = (int) ev.getY();
mOffsetLeft = (int) ev.getRawX() - mDownX;
mOffsetTop = (int) ev.getRawY() - mDownY;
Log.i(TAG, "手指按下ev.getX---------->" + mDownX + " ev.getY-------->"
+ mDownY);
mDragPosition = pointToPosition(mDownX, mDownY);
if (mDragPosition == -1) {
return super.dispatchTouchEvent(ev);
}
mStartDragItemView = getChildAt(mDragPosition);
if (mStartDragItemView == null) {
return super.dispatchTouchEvent(ev);
}
// 500秒长按认为是拖动事件
handler.removeMessages(LONG_PRESS);
handler.sendEmptyMessageDelayed(LONG_PRESS, dragRespondTime);
break;
case MotionEvent.ACTION_MOVE:
if (isDrag && mDragImageView != null) {
int newMoveX = (int) ev.getX();
int newmoveY = (int) ev.getY();
updateDragImage(newMoveX, newmoveY);
if (Math.abs(moveX - newMoveX) < 10 && Math.abs(moveY - newmoveY) < 10) {
//认为没有移动
// moveX = newMoveX;
// moveY = newmoveY;
}else {
moveX = newMoveX;
moveY = newmoveY;
handler.removeMessages(HOVER);
handler.sendEmptyMessageDelayed(HOVER, HOVER_TIEM);
}
}
break;
case MotionEvent.ACTION_UP:
handler.removeMessages(LONG_PRESS);
handler.removeMessages(HOVER);
moveX = (int) ev.getX();
moveY = (int) ev.getY();
if (isDrag && mDragImageView != null) {
// handler.removeCallbacks(scrollRunnable);
isDrag = false;
mStartDragItemView.setBackgroundResource(R.drawable.button_selector);
windowManager.removeView(mDragImageView);
createAnimation();
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* 拖动时添加镜像
*
* @param mDragBitmap
* @param mDownX
* @param mDownY
*/
private void createDragImage(Bitmap mDragBitmap, int mDownX, int mDownY) {
// TODO Auto-generated method stub
winLayoutParams = new WindowManager.LayoutParams();
winLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
Log.i(TAG, "createDragImage ev.getX---------->" + mDownX
+ " ev.getY-------->" + mDownY);
winLayoutParams.x = -mStartDragItemView.getWidth()/2 + mDownX + mOffsetLeft;
winLayoutParams.y = -mStartDragItemView.getHeight() + 15 + mDownY + mOffsetTop - mStatusHeight;
winLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
winLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
winLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mDragImageView = new ImageView(getContext());
mDragImageView.setImageBitmap(mDragBitmap);
// 添加到wm管理中
windowManager.addView(mDragImageView, winLayoutParams);
}
/**
* 拖动item界面更新
*/
private void updateDragImage(int moveX, int moveY) {
winLayoutParams.x = -mStartDragItemView.getWidth()/2 + moveX + mOffsetLeft;
winLayoutParams.y = -mStartDragItemView.getHeight() +15 + moveY + mOffsetTop - mStatusHeight;
windowManager.updateViewLayout(mDragImageView, winLayoutParams);
}
/**
* Rectangle used for hit testing children
*/
private Rect mTouchFrame;
/**
* 动画过程中,这个方法不准确
* @param x
* @param y
* @return
*/
public int pointToPosition(int x, int y) {
if (changing) {
return -1;
}
Rect frame = mTouchFrame;
if (frame == null) {
mTouchFrame = new Rect();
frame = mTouchFrame;
}
final int count = getChildCount();
for (int i = count - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getVisibility() == View.VISIBLE && child.getAnimation() == null) {
child.getHitRect(frame);
if (frame.contains(x, y)) {
return i;
}
}
}
return -1;
}
/**
* 获取状态栏高度
*
* @param context
* @return
*/
public static int getStatusHeight(Context context) {
int statusHeight = 0;
Rect rect = new Rect();
((Activity) context).getWindow().getDecorView()
.getWindowVisibleDisplayFrame(rect);
statusHeight = rect.top;
if (statusHeight == 0) {
Class<?> localClass;
try {
localClass = Class.forName("com.android.internal.R$dimen");
Object localObject = localClass.newInstance();
int i5 = Integer.parseInt(localClass
.getField("status_bar_height").get(localObject)
.toString());
statusHeight = context.getResources().getDimensionPixelSize(i5);
} catch (Exception e) {
e.printStackTrace();
}
}
return statusHeight;
}
public interface ChangeItemCallback{
void changeItem(int indexEnd, View childView);
}
private ChangeItemCallback mCallback;
public void setChangeItemCallback(ChangeItemCallback callback) {
this.mCallback = callback;
}
}
这里硬贴两个类确实很冗余,我的demo是处理在一个项目中的子页面所以就不直接上传完成项目了。
这里开始是重点讲解了:
重点内容
1、 前一篇博客中已经将点击交换给做了,所以这里就不太谈论这部分,但是这里对之前的效果进行了优化。
2、这一篇主要处理点击拖拽,拖拽控件交换位置,松手回弹。
那么本文参考了这篇文章:
http://blog.csdn.net/adfsadsfa/article/details/50630470
用window 实现拖拽的控件,这个思路还是可以的。
这里大致以两个事件展开-长按和悬停。长按识别创建拖拽的动画控件,悬停处理控件交换。
事件处理在dispatchTouchEvent
中监听
细节这里似乎没有什么特别难的地方以下随便挑几处说道说道:
public void createAnimation() {
PropertyValuesHolder pvhTransX =
PropertyValuesHolder.ofFloat("translationX", -mStartDragItemView.getWidth()/2 + moveX - mStartDragItemView.getLeft(), 0);
PropertyValuesHolder pvhTransY =
PropertyValuesHolder.ofFloat("translationY", -mStartDragItemView.getHeight() +15 + moveY - mStartDragItemView.getTop(), 0);
final ObjectAnimator animIn = ObjectAnimator.ofPropertyValuesHolder(
mStartDragItemView, pvhTransX, pvhTransY).
setDuration(500);
animIn.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
}
});
animIn.start();
}
这个方法用来处理拖拽结束后,回弹的动画,发现translation 这个东西非常好用,主要是目标点填0就好了。
-mStartDragItemView.getWidth()/2 + moveX
这里不是直接用的 moveX 是因为做了一个简单的偏移效果,腾讯新闻中也是有的。这个很有意义。y轴同
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case LONG_PRESS:
// 绘图缓存
mStartDragItemView.setDrawingCacheEnabled(true);
// 创建镜像
Bitmap mDragBitmap = Bitmap.createBitmap(mStartDragItemView
.getDrawingCache());
// 释放缓存
mStartDragItemView.destroyDrawingCache();
isDrag = true;
createDragImage(mDragBitmap, mDownX, mDownY);
mStartDragItemView.setBackgroundResource(R.drawable.btn_bg_dashgap);
break;
case HOVER:
if ( !changing) {
// Logger.i(TAG, "dispatchTouchEvent: " + moveX + " : " + moveY, "oddshou");
int index = pointToPosition(moveX, moveY);
int indexStart = indexOfChild(mStartDragItemView);
if (index != -1 && index != indexStart && mCallback != null) {
Logger.i(TAG, "dispatchTouchEvent: " + indexStart + " : " + index, "oddshou");
changing = true;
mCallback.changeItem(index, mStartDragItemView);
mDragPosition = index;
}
changing = false;
}
break;
}
}
};
长按500ms判定,悬停200ms
这里使用悬停之前,用过直接在dispatch 的move中判断,但是效果非常不理想,改用悬停判断效果还可以。
这里再说一下bug,未完美的地方:
1、拖拽结束回弹的时候原控件(虚线框)会消失,因为它跑去做动画了。
2、translation 动画会被其他控件遮挡,这主要是 view zorder的问题,我这里也得不到很好的解决。
以上两个问题,若仁兄有解决方案还请告知。留言或者email。oddshou@sina.com