仿网易新闻顶部菜单html,仿网易新闻标签栏(动画拖动变化标签位置)

网易新闻标签栏的实现效果我一直想实现试试,最近花了点时间终于实现,初步实现效果如下,后面有时间还会继续完善(最近突然发现其实可以通过RecyclerView实现,但我这种实现方式也算是一种学习和练习吧)

8fa26b4a6544

效果

详细Demo和源码这里给出GitHub地址。

TabMoveLayout

实现功能

1.长按抖动

2.标签可随意拖动,其他标签随之变换位置

3.拖动变换子View顺序

难点:

1.熟悉自定义ViewGroup过程,onMeasure、onLayout

2.ViewGroup事件处理

3.多种拖动情况考虑(位置移动计算)

4.ViewGroup中子View的变更替换添加

实现思路:

1.自定义ViewGroup,实现标签栏的排列,这里我以4列为例(onMeasure,onLayout)

2.实现触摸标签的拖动,通过onTouch事件,在DOWN:获取触摸的x,y坐标,找到被触摸的View,在MOVE:通过view.layout()方法动态改变View的位置

3.其他标签的位置变换,主要通过TranslateAnimation,在MOVE:找到拖动过程中经过的View,并执行相应的Animation

(这里重点要考虑清楚所有拖动可能的情况)

4.拖动结束后,随之变换ViewGroup中view的实际位置,通过removeViewAt和addView进行添加和删除,中间遇到一点问题(博客)已分析。

关键代码:

1.自定义ViewGroup

这里主要是onMeasure和onLayout方法。这里我要说一下我的布局方式

/**

* 标签个数 4

* |Magin|View|Magin|Magin|View|Magin|Magin|View|Magin|Magin|View|Magin|

* 总宽度:4*(标签宽度+2*margin) 按照比例 (总份数):4*(ITEM_WIDTH+2*MARGIN_WIDTH)

* 则一个比例占的宽度为:组件总宽度/总份数

* 一个标签的宽度为:组件宽度/总份数 * ITEM_WIDTH(宽度占的比例)

* 一个标签的MARGIN为:组件宽度/总份数 * MARGIN_WIDTH(MARGIN占的比例)

* 行高=(ITEN_HEIGHT+2*MARGIN_HEIGHT)*mItemScale

* 一个组件占的宽度=(ITEM_WIDTH + 2*MARGIN_WIDTH)*mItemScale

*/

可能看起来比较复杂,其实理解起来就是:

一个标签所占的宽度=标签的宽度+2marginwidth

一个标签所占的高度=标签的高度+2marginheight

这里都是用的权值计算的

一个比例占的长度为=总宽度/总份数

假如屏幕宽度为1000px,标签的宽度占10份,marginwidth占2份,标签的高度占5份,marginheight占1份

一个比例所占的长度(以一行4个标签为例) = 1000/((10+22)4)

一个标签所占的宽度 = (10+22)一个比例所占的长度

一个标签所占的高度 = (5+21)一个比例所占的长度

onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);

int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

int modeWidth = MeasureSpec.getMode(widthMeasureSpec);

int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

int width;

int height;

int childCount = getChildCount();

if (modeWidth == MeasureSpec.EXACTLY) {

width = sizeWidth;

} else {

width = Math.min(sizeWidth, getScreenWidth(mContext));

}

if (modeHeight == MeasureSpec.EXACTLY) {

height = sizeHeight;

} else {

int rowNum = childCount / ITEM_NUM;

if (childCount % ITEM_NUM != 0) {

height = (int) Math.min(sizeHeight, (rowNum + 1) * (ITEM_HEIGHT + 2 * MARGIN_HEIGHT) * mItemScale);

} else {

height = (int) Math.min(sizeHeight, rowNum * (ITEM_HEIGHT + 2 * MARGIN_HEIGHT) * mItemScale);

}

}

measureChildren(

MeasureSpec.makeMeasureSpec((int) (mItemScale * ITEM_WIDTH), MeasureSpec.EXACTLY),

MeasureSpec.makeMeasureSpec((int) (mItemScale * ITEM_HEIGHT), MeasureSpec.EXACTLY));

setMeasuredDimension(width, height);

}

这里也是自定义View常见的一个点,注意MeasureSpace的三种模式EXACITY,AT_MOST,UNSPECIFIED,三种模式的对应关系可以简单理解为:

EXACITY -> MATCH_PARENT或者具体值

AT_MOST -> WARP_CONTENT

UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView,通过measure方法传入的模式。

所以这里我处理方式为

宽度:当EXACITY时:width = widthsize,当其他模式时,width=sizewidth和屏幕宽度的较小值(这里注意sizeWidth的值为父组件传给自己的宽度值,所以如果当前组件处于第一层级,sizeWidth=屏幕宽度)

高度:当EXACITY时:height = heightsize,当其他模式时,计算行数,height=行数一行的高度(height+2marginheight)

再执行measureChildren

onLayout方法

protected void onLayout(boolean changed, int l, int t, int r, int b) {

int childCount = getChildCount();

int left;

int top;

int right;

int bottom;

for (int i = 0; i < childCount; i++) {

int row = i / ITEM_NUM;

int column = i % ITEM_NUM;

View child = getChildAt(i);

left = (int) ((MARGIN_WIDTH + column * (ITEM_WIDTH + 2 * MARGIN_WIDTH)) * mItemScale);

top = (int) ((MARGIN_HEIGHT + row * (ITEM_HEIGHT + 2 * MARGIN_HEIGHT)) * mItemScale);

right = (int) (left + ITEM_WIDTH * mItemScale);

bottom = (int) (top + ITEM_HEIGHT * mItemScale);

child.layout(left, top, right, bottom);

}

}

所以onlayout也就比较好理解了,利用for循环遍历child,计算每个child所在的行和列,再通过child.layout()布局。

2.onTouch事件

public boolean onTouchEvent(MotionEvent event) {

float x = event.getX();

float y = event.getY();

if(isMove){

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

mBeginX = x;

mBeginY = y;

mTouchIndex = findChildIndex(x, y);

mOldIndex = mTouchIndex;

if (mTouchIndex != -1) {

mTouchChildView = getChildAt(mTouchIndex);

mTouchChildView.clearAnimation();

//mTouchChildView.bringToFront();

}

break;

case MotionEvent.ACTION_MOVE:

if (mTouchIndex != -1 && mTouchChildView != null) {

moveTouchView(x, y);

//拖动过程中的View的index

int resultIndex = findChildIndex(x, y);

if (resultIndex != -1 && (resultIndex != mOldIndex)

&& ((Math.abs(x - mBeginX) > mItemScale * 2 * MARGIN_WIDTH)

|| (Math.abs(y - mBeginY) > mItemScale * 2 * MARGIN_HEIGHT))

) {

beginAnimation(Math.min(mOldIndex, resultIndex)

, Math.max(mOldIndex, resultIndex)

, mOldIndex < resultIndex);

mOldIndex = resultIndex;

mOnHover = true;

}

}

break;

case MotionEvent.ACTION_UP:

setTouchIndex(x, y);

mOnHover = false;

mTouchIndex = -1;

mTouchChildView = null;

return true;

}

}

return super.onTouchEvent(event);

}

这个方法算是这个效果的主要方法了,详细分析一下吧。首先看DOWN事件

case MotionEvent.ACTION_DOWN:

mBeginX = x;

mBeginY = y;

mTouchIndex = findChildIndex(x, y);

mOldIndex = mTouchIndex;

if (mTouchIndex != -1) {

mTouchChildView = getChildAt(mTouchIndex);

mTouchChildView.clearAnimation();

//mTouchChildView.bringToFront();

}

break;

可以看到,首先我先记录了触摸位置的x,y坐标,通过findChildIndex方法确定触摸位置的child的index。

/**

* 通过触摸位置确定触摸位置的View

*/

private int findChildIndex(float x, float y) {

int row = (int) (y / ((ITEM_HEIGHT + 2 * MARGIN_HEIGHT) * mItemScale));

int column = (int) (x / ((ITEM_WIDTH + 2 * MARGIN_WIDTH) * mItemScale));

int index = row * ITEM_NUM + column;

if (index > getChildCount() - 1) {

return -1;

}

return index;

}

因为最初分析的时候已经说到了

一行的高度 = 组件的高度+2marginheight

一列的宽度 = 组件的宽度+2marginwidth

所以当我们得到触摸位置的x,y,就可以通过y/行高得到行数,x/列宽

当触摸位置没有child时返回-1。

得到触摸坐标后,获得通过getChildAt()获得触摸坐标的child,通过clearAnimation停止抖动。

MOVE事件:

case MotionEvent.ACTION_MOVE:

if (mTouchIndex != -1 && mTouchChildView != null) {

moveTouchView(x, y);

//拖动过程中的View的index

int resultIndex = findChildIndex(x, y);

if (resultIndex != -1 && (resultIndex != mOldIndex)

&& ((Math.abs(x - mBeginX) > mItemScale * 2 * MARGIN_WIDTH)

|| (Math.abs(y - mBeginY) > mItemScale * 2 * MARGIN_HEIGHT))

) {

beginAnimation(Math.min(mOldIndex, resultIndex)

, Math.max(mOldIndex, resultIndex)

, mOldIndex < resultIndex);

mOldIndex = resultIndex;

mOnHover = true;

}

}

break;

首先根据move过程中的x,y,通过moveTouchView移动拖动的view随手指移动。

private void moveTouchView(float x, float y) {

int left = (int) (x - mTouchChildView.getWidth() / 2);

int top = (int) (y - mTouchChildView.getHeight() / 2);

mTouchChildView.layout(left, top

, (left + mTouchChildView.getWidth())

, (top + mTouchChildView.getHeight()));

mTouchChildView.invalidate();

}

这里有个细节,在移动的时候,将触摸的位置移动到大概child的中心位置,这样看起来正常一下,也就是我对x和y分别减去了child宽高的一半,不然会使得手指触摸的位置一直在child的左上角(坐标原点),看起来很变扭。最后通过layout和invalidate方法重绘child。

移动其他view

这个应该算是这个组件最难实现的地方,我在这上面花了最长的时间。

1)首先什么时候执行位移动画,反过来想就是什么时候不执行位移动画

这里分了四种情况:

(1)拖动的位置没有标签,也就是图上的从标签9往右拖

(2)拖动的位置和上一次位置相同(也就是没动)

(3)移动的位置不到一行的高度(也就是没有脱离当前标签的区域)

(4)移动的位置不到一列的宽度(也就是没有脱离当前标签的区域)

2)执行位移动画,下面会分析

3)mOldIndex = resultIndex这里是为了保存上一次移动的坐标位置

4)mOnHover=true,记录拖动不放的情况(和拖动就释放的情况有区分)

/**

* 移动动画

*

* @param forward 拖动组件与经过的index的前后顺序 touchindex < resultindex

* true-拖动的组件在经过的index前

* false-拖动的组件在经过的index后

*/

private void beginAnimation(int startIndex, int endIndex, final boolean forward) {

TranslateAnimation animation;

ViewHolder holder;

List animList = new ArrayList<>();

int startI = forward ? startIndex + 1 : startIndex;

int endI = forward ? endIndex + 1 : endIndex;//for循环用的是

if (mOnHover) {//拖动没有释放情况

if (mTouchIndex > startIndex) {

if (mTouchIndex < endIndex) {

startI = startIndex;

endI = endIndex + 1;

} else {

startI = startIndex;

endI = endIndex;

}

} else {

startI = startIndex + 1;

endI = endIndex + 1;

}

}

//X轴的单位移动距离

final float moveX = (ITEM_WIDTH + 2 * MARGIN_WIDTH) * mItemScale;

//y轴的单位移动距离

final float moveY = (ITEM_HEIGHT + 2 * MARGIN_HEIGHT) * mItemScale;

//x轴移动方向

final int directX = forward ? -1 : 1;

final int directY = forward ? 1 : -1;

boolean isMoveY = false;

for (int i = startI; i < endI; i++) {

if (i == mTouchIndex) {

continue;

}

final View child = getChildAt(i);

holder = (ViewHolder) child.getTag();

child.clearAnimation();

if (i % ITEM_NUM == (ITEM_NUM - 1) && !forward

&& holder.row == i / ITEM_NUM && holder.column == i % ITEM_NUM) {

//下移

holder.row++;

isMoveY = true;

animation = new TranslateAnimation(0, directY * (ITEM_NUM - 1) * moveX, 0, directX * moveY);

} else if (i % ITEM_NUM == 0 && forward

&& holder.row == i / ITEM_NUM && holder.column == i % ITEM_NUM) {

//上移

holder.row--;

isMoveY = true;

animation = new TranslateAnimation(0, directY * (ITEM_NUM - 1) * moveX, 0, directX * moveY);

} else if (mOnHover && holder.row < i / ITEM_NUM) {

//onHover 下移

holder.row++;

isMoveY = true;

animation = new TranslateAnimation(0, -(ITEM_NUM - 1) * moveX, 0, moveY);

} else if (mOnHover && holder.row > i / ITEM_NUM) {

//onHover 上移

holder.row--;

isMoveY = true;

animation = new TranslateAnimation(0, (ITEM_NUM - 1) * moveX, 0, -moveY);

} else {//y轴不动,仅x轴移动

holder.column += directX;

isMoveY = false;

animation = new TranslateAnimation(0, directX * moveX, 0, 0);

}

animation.setDuration(mDuration);

animation.setFillAfter(true);

final boolean finalIsMoveY = isMoveY;

animation.setAnimationListener(new Animation.AnimationListener() {

@Override

public void onAnimationStart(Animation animation) {

}

@Override

public void onAnimationEnd(Animation animation) {

child.clearAnimation();

if (finalIsMoveY) {

child.offsetLeftAndRight((int) (directY * (ITEM_NUM - 1) * moveX));

child.offsetTopAndBottom((int) (directX * moveY));

} else {

child.offsetLeftAndRight((int) (directX * moveX));

}

}

@Override

public void onAnimationRepeat(Animation animation) {

}

});

child.setAnimation(animation);

animList.add(animation);

}

for (TranslateAnimation anim : animList) {

anim.startNow();

}

}

位移动画,这段代码怎么解释哪...我写的时候是发现一个bug改一种情况,最后实现了这段代码。

8fa26b4a6544

初步效果

1)这里首先确定开始位移的view的坐标和结束位移的坐标

这里分为两种情况:

case1:手指拖动后抬起(down->move->up);

case2:手指来回拖动不放(down->move->move)

case1:是常见情况,这里我们就可以按照forward再分为两种情况

case1.1:标签0->标签1(forward =true);

case1.2:标签5->标签1(forward=false)

case1.1:

标签0移动到标签1,标签0随手指移动,所以需要执行位移动画的只有标签1,所以startI = 1,endI = 2(for循环

所以forward = true,startI = startIndex+1,endI=endIndex+1;

case1.2:

标签4移动到标签0,标签4随手指移动,所以需要执行位移动画的是标签0~标签3,所以startI=0,endI=4,所以而startindex=0,endindex=5;

所以forward = false,startI = startIndex,endI = endIndex

case2:是指手指拖动不放,来回拖动,所以通过mOnHover=true参数来确定是否是拖动没放情况,这里面又要细分为三种情况

case2.1:标签0->标签2->标签1,将标签0拖动到2,再回到0的位置,这是标签0一直随手指移动,

后面这段动画,startindex = 1,endindex = 2,touchindex = 0,只有标签2需要执行动画,标签1不动,所以startI = 2,endI = 3

所以mOnHover = true,touchindex

case2.2:标签8->标签4->标签5,将标签8拖到4,在拖到5的位置,后面这段动画,startindex = 4,endindex = 5,touchindex = 8,只有标签4需要执行动画,其他标签不动,所以startI=4,endI=5

所以mOnHover = true,startIndex

case2.3:标签8->标签4->标签5->标签9,后面这段动画,startindex = 5,endindex = 9,touchindex = 8,执行动画的有标签5~标签9,所以startI=5,endI=10

所以mOnHover=true.startIndex

接下来就是for循环的移动动画,可以看到这里可以大致分为三种情况

case1:X轴的平移动画,Y轴不动;

case2:Y轴上移一行,X轴左移三个(也就是一行的第一个上移到上一行的最后一个);

case3:Y轴下移一行,X轴右移三个(也就是一行的最后一个下移到下一行的第一个);

可以看到我还是总体分为了mOnHover=false和mOnHover=true两种情况,我在初始化时,将每个child的所在行和列以Tag的形式保存到了child中

if (i % ITEM_NUM == (ITEM_NUM - 1) && !forward

&& holder.row == i / ITEM_NUM && holder.column == i % ITEM_NUM) {

//下移

holder.row++;

isMoveY = true;

animation = new TranslateAnimation(0, directY * (ITEM_NUM - 1) * moveX, 0, directX * moveY);

} else if (i % ITEM_NUM == 0 && forward

&& holder.row == i / ITEM_NUM && holder.column == i % ITEM_NUM) {

//上移

holder.row--;

isMoveY = true;

animation = new TranslateAnimation(0, directY * (ITEM_NUM - 1) * moveX, 0, directX * moveY);

} else if (mOnHover && holder.row < i / ITEM_NUM) {

//onHover 下移

holder.row++;

isMoveY = true;

animation = new TranslateAnimation(0, -(ITEM_NUM - 1) * moveX, 0, moveY);

} else if (mOnHover && holder.row > i / ITEM_NUM) {

//onHover 上移

holder.row--;

isMoveY = true;

animation = new TranslateAnimation(0, (ITEM_NUM - 1) * moveX, 0, -moveY);

} else {//y轴不动,仅x轴移动

holder.column += directX;

isMoveY = false;

animation = new TranslateAnimation(0, directX * moveX, 0, 0);

}

case1:当是一行的最后一个,forward=false(后面的标签往前挤),标签的Tag中的x,y没有变化(也就是第一次拖动和mOnHover=true区分),这时下移

case2:当是一行的第一个,forward=true(上面的标签往下挤),标签的Tag中的x,y没有变化(也就是第一次拖动和mOnHover=true区分),这时上移

case3:当mOnHover=true,标签当前所在行

case4:当mOnHover=true,标签当前所在行>标签初始所在行,这时上移

case5:X轴的平移,y轴不动

后面设置了child的动画监听,当动画结束后,需要将child的实际位置设置为当前位置(因为这里用的不是属性动画,所以执行动画后child的实际位置并没有变化,还是原始位置)

UP事件:

case MotionEvent.ACTION_UP:

setTouchIndex(x, y);

mOnHover = false;

mTouchIndex = -1;

mTouchChildView = null;

return true;

这里主要看setTouchIndex事件

/**

* ---up事件触发

* 设置拖动的View的位置

* @param x

* @param y

*/

private void setTouchIndex(float x,float y){

if(mTouchChildView!= null){

int resultIndex = findChildIndex(x, y);

Log.e("resultindex", "" + resultIndex);

if(resultIndex == mTouchIndex||resultIndex == -1){

refreshView(mTouchIndex);

}else{

swapView(mTouchIndex, resultIndex);

}

}

}

可以看到,这里拖动结束后就需要将拖动位置变化的child实际改变它在ViewGroup中的位置

这里有两种情况

case1:拖动到最后,child的顺序没有改变,只有touchview小浮动的位置变化,这时只需要刷新touchview即可

case2:将位置变换的child刷新其在viewgroup中的顺序。

/**

*刷新View

* ------------------------------重要------------------------------

* 移除前需要先移除View的动画效果,不然无法移除,可看源码

*/

private void refreshView(int index) {

//移除原来的View

getChildAt(index).clearAnimation();

removeViewAt(index);

//添加一个View

TextView tv = new TextView(mContext);

LayoutParams params = new ViewGroup.LayoutParams((int) (mItemScale * ITEM_WIDTH),

(int) (mItemScale * ITEM_HEIGHT));

tv.setText(mData.get(index));

tv.setTextColor(TEXT_COLOR);

tv.setBackgroundResource(ITEM_BACKGROUND);

tv.setGravity(Gravity.CENTER);

tv.setTextSize(TypedValue.COMPLEX_UNIT_PX,TEXT_SIZE);

tv.setTag(new ViewHolder(index / ITEM_NUM, index % ITEM_NUM));

this.addView(tv,index ,params);

tv.startAnimation(mSnake);

}

刷新index的View,这里有个需要注意的点,因为每个child都在执行抖动动画,这时候直接removeViewAt是没有办法起效果的,需要先clearAnimation再执行,具体我已经写了一篇博客从源码分析了

Animation导致removeView无效(源码分析)

private void swapView(int fromIndex, int toIndex) {

if(fromIndex < toIndex){

mData.add(toIndex+1,mData.get(fromIndex));

mData.remove(fromIndex);

}else{

mData.add(toIndex,mData.get(fromIndex));

mData.remove(fromIndex+1);

}

for (int i = Math.min(fromIndex, toIndex); i <= Math.max(fromIndex, toIndex); i++) {

refreshView(i);

}

}

这里交换touch和最终位置的child,所以首先实际改变Data数据集,再利用for循环,通过refreshView函数,刷新位置变化的child。

实现过程比较坎坷,但也是一种实现思路供大家参考吧,写完后对于自定义ViewGroup和动画能有很好的学习,这里再放一下Github地址TabMoveLayout,喜欢的可以star一下~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值