自定義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);後寬度變化字顯示不開造成的變化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章