自定义View的一些问题

最近在实现一个拖拽添加View并拖动删除的组件,分成两个部分(尚未完成的)和SlideBlock,代码在我的github上,需要自取

在这个过程中,我总结了一下自定View的一些问题,分为两个部分阐述:

第一部分:绘制
SlideBlock继承自LinearLayout
1、构造器

//继承自LinearLayout之后需要构造函数,一般两个就够用了
public SlideBlock(Context context) {
    this(context,null);
}
public SlideBlock(Context context, @Nullable AttributeSet attrs){
    super(context, attrs);
    mContext = context;
}

2、组件宽高获取
我们都知道一个View的三大绘制流程:onMeasure()、onLayout()、onDraw();
其中onMeasure我以前很喜欢用,后来发现它有走多次的问题,而且第一次结果永远是0,第二次结果可能不对,第三次才是正确的,这一次我改用在三次onMeasure之后执行的onSizeChange()确定宽高,只走一次还准确,但问题也是有的,我目前没考虑到组件变化的问题。
此时我们已经获得了组件的宽高,然后可以调用函数进行绘制,注意onLayout()虽然也可以反应组件的宽高,但在祂里面在确定就有些晚了,于是我们在onSizeChange()中调用自定义绘制函数setView()

3、绘制
因为组件继承自LinearLayout,所以本次采用addView的方式添加,而非onDraw中draw的方式绘制,然后我们可以直接设置背景
0)容器属性
setOrientation(LinearLayout.HORIZONTAL);
setClipChildren(false);
setClipToPadding(false);
1)清空界面removeAllViews();
2)添加组件addView(组件View,new LayoutParams(宽度, 高度))
此处要注意高度全屏可以直接写可以写作LayoutParams.MATCH_PARENT,因为此处设置是要在之后轮询childView时才会生效
3)设置child宽高
循环设置child的宽高
View child = getChildAt(i);
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth,MeasureSpec.EXACTLY);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight,MeasureSpec.EXACTLY);
child.measure(widthMeasureSpec, childHeightMeasureSpec);
需要注意此处childWidth和childHeight不能用LayoutParams.MATCH_PARENT了需要从屏幕宽度计算出具体值
4)onLayout
执行顺序是:onMeasure()->onSizeChanged()->、onLayout()->onDraw()
这里有一个问题:继承自ViewGroup的自定义组件内容不显示 ,解决的话有两个关键点:

1)addView之后需要遍历设置child的宽高,设置后在onlayout中仍可能为零,但不设置的child就显示不出来
for (int i = 0; i < getChildCount(); i++) {
    final View child = getChildAt(i);
    if(desiredWidth!=0) {
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(desiredHeight,MeasureSpec.EXACTLY);
        child.measure(widthMeasureSpec, childHeightMeasureSpec);
    }
}
2)onLayout中需要遍历设置child的位置
for (int i = 0; i < count; i++) {
    final View child = getChildAt(i);
    if (child.getVisibility() == View.GONE) {
        continue;
    }
    final int width = child.getMeasuredWidth();//可能为0
    final int height = child.getMeasuredHeight();//可能为0
    //设置child的占位
    child.layout(childLeft, childTop, childLeft + rowWidth, childTop + height);
    childLeft+=rowWidth;
}

上述例子中是在一个继承自LinearLayout的组件中横向添加了两个LinearLayout,所以才增加left的变量值,组件中这里做了mode处理
需要注意的是,只要遍历并设置组件的第一层child的宽高就可以了,不论child是View还是ViewGroup都可以愉快的解决问题

onLayout()的默认值是组件在屏幕中的位置,这个函数本身就是定位用的,把确定好宽高的child逐一取出,设置
child.layout(childLeft, childTop, childRight, childBottom);

4、属性
经过测试,所有activity/fragment中设置的属性会先被执行,然后再走onSizeChange()

5、问题点
在写的时候发现几处问题特此说明一下
1)子View包含元素过大时如何显示
如父控件设置为横向,当横向宽度过大时子View只显示部分
所以需要通过
ll.addView(createTextView(),new LayoutParams(rowWidth, rowHeight));
设置子View的宽高

2)自定义View时LinearLayout ll = new LinearLayout(mContext);
创建对象后获取组件ll的getLayoutParams()为null;
所以此处只能通过setLayoutParams(new LayoutParams(rowWidth, rowHeight))

3)布局是否应当写在onDraw?
当用组件搭建可以完成时不需要onDraw,但如果涉及到path,convan就需要了

4)在后面的移动中发现一个问题
平移中View的高度变小了,这个现象很奇怪,侦测view.getHeight()没有变化,也不是margin导致的,最后是同事提醒你的内容文字折行了我才发现是因为文字导致view的背景色范围出现问题,如图所示:
这是一张移动的textView高度小于其他textView的图

第二部分:事件处理

众所周知View的触摸机制,而我的组件只是判断滑动距离,因此只对组件的onTouchEvent做处理就满足需求。
说一下思路:
首先我获取了event.getX()和event.getY());他们是组件内的X/Y值,与此相对的还有getRawX(),getRawY() 他们是屏幕上的X/Y值
然后根据运算获取到点击的是哪一行,再逐一获取View的X/Y从而判断我们应当处理的那个View。
但此处发生了问题,对View的诸多X/Y函数一头雾水,在附录总结一下
关于view的平滑移动我试了几套方案

方案一:
因为众所周知的原因,一开始我使用setleft/setright来修改组件位置,效果能达成,可结果出现一个问题,组件的内容不会随着居中,很生硬也很难看,本想通过给view动态setGravity的方式解决,试了一下搞不定,翻了翻google他说你应该用getTranslationX而不是setLeft。
还是记录一下代码:

TextView leftView = nowList.get(number-1);
//当前操作view左移
moveText.setLeft(moveText.getLeft()+Math.round(moveSize));
//操作view左侧的右侧变短
leftView.setRight(leftView.getRight()+Math.round(moveSize));

方案二:
使用setTranslationX做修改
这个方案解决了文字居中的痛点,但是移动时会有忽大忽小的白边,经过多次测试这个空白仍然无法消除,但相对于其他方案而言这个效果还可以接受,目前推测是因为setTranslationX为float和set X这种int转换造成误差的缘故

if(olp.width - moveSize>0) {//先做判断看是修改还是移除
    //先修改右侧的偏移
    rightTextView.setTranslationX(moveSize);
    //然后修改容器的宽度
    olp.width = olp.width - moveSize;
    rightTextView.setLayoutParams(olp);
}else{
    removeView(rightTextView);//移除指定view
    nowList.remove(rightTextView);//从缓存中释放
    resetLeftRight();//重置左右两部分    
}
//再修改当前的偏移
olp = (LayoutParams) moveText.getLayoutParams();
//olp.width = getNowWidth(moveText);
//olp.width = rightTextView.getLeft()+moveSize -moveText.getLeft();//如此这般发现宽度没有变化
olp.width = olp.width + Math.round(moveSize);
moveText.setLayoutParams(olp); 

上面可以看出对于olp.width的获取我使用几种方式:
1)getNowWidth:是一个循环取出并累加当前容器内,除指定view外所有view的getWidth()的函数,就结果而言不能解决白边的问题,他的结果和view.getWidth()是一致的
2)rightTextView.getLeft()+moveSize -moveText.getLeft();
这个方法摒除了float造成的误差,但实际来说也没有解决问题
3)olp.width + Math.round(moveSize);目前采用的方式,空白仍然存在

网络方案:不能解决问题
1)scrollBy
rightTextView.scrollBy(-moveSize,0);
moveText.scrollBy(-moveSize,0);
经过测试上面这个方案只有内容平移了,组件没有动
2)offsetLeftAndRight
rightTextView.offsetLeftAndRight(-moveSize);
moveText.offsetLeftAndRight(-moveSize);
经过测试上面这个方案不能实现组件宽度的变化,它是通过将组件平移实现效果

最后我们整理一下android中view坐标相关的知识

1、getX() getY()
这个是view左上角距离父布局的距离,而且这个距离可能会变化,比如使用动画将view移动的时候,这两个坐标就会发生变化。

2、getTranslationX() getTranslationY()
view相对于最初位置的变化量。始终是相对于最初的位置。
同时我们也可以使用set方法比如setTranslationX来动态改变view的位置。所以这一组坐标存在的意义就是为了view的位置变化使用的。

3、getLeft() getTop() getRight() getBottom()
这四个坐标是指一个view的边际距离父布局的距离。
Getleft()和getRight()是相对父布局的左边,而getTop()和getBootom()是相对于父布局的上边。所以我们通过这四个值是可以知道view的宽度和高度的。
需要注意相接的两个组件A和B,A.getRight==B.getLeft

这三组坐标的关系:getX()= getTranslationX()+getLeft()

4、getPivotX() getPivotY()
view旋转和缩放的时候的中心点,需要注意的是它的值等于宽度的一半,如果要判断这个点在屏幕的位置还需要(view.getX()+view.getPivotX())

经过组合测试发现(下面返回的值都是px单位)
修改view的距离用setLeft() setTop() setRight() setBottom() 时不会让内容居中,google也不推荐
修改view的距离用setTranslationX() setTranslationY() 时通过偏移量改变组件X,y值,结合movelp.width的方式改变组件大小时会出现局部空白的情况,经过测试发现是修改的width没有及时反映到getWidth()
然后我查了一下区别
getLayoutParams().width返回的是该view向父view请求的最大宽度,不是view实际绘画的宽度,getMeasuredWidth()与它等效
getWidth()获取的就是该view的实际宽度
第次改变一个viewgroup中的view宽度需要通过设置getLayoutParams().width和setWidth()的方式,此种方式在快速拉取时也会出现空白
最后结论上方空白是由于view.setText(“text”+text);后宽度变化字显示不开造成的变化

发布了66 篇原创文章 · 获赞 5 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/yuemitengfeng/article/details/80429574
今日推荐