從來都沒想過這個屬性會引起bug
問題描述
可以看這里http://www.itdecent.cn/p/ff9df7c392e9
是在寫上邊的功能的時候碰到的。最早我是沒問題的,好像是我把狀態(tài)欄弄成透明以后就發(fā)現(xiàn)出問題了。每次進入頁面recyclerview會自動往上滾動一段距離,
奇怪,而且監(jiān)聽onScrollStateChanged可以發(fā)現(xiàn)狀態(tài)進去就成了2,也就是setting狀態(tài),可我根本沒進行任何操作啊,我就設置了下數(shù)據(jù)。
然后就開始瞎折騰,想著把監(jiān)聽刪了,額,監(jiān)聽是在滾動以后觸發(fā)的,所以其實不設置監(jiān)聽它也滾動。想著是不是我給第一個item設置的addItemDecoration那個top 300的問題?我中間有次改成200發(fā)現(xiàn)好了??晌腋幕?00又不行了,反正不知道是為啥,就放那不管了。
今早想著再試試,我就找了別的類,完事把那個類的item也改成這個類的item發(fā)現(xiàn)那個類的recyclerview也會自動滾上去。。
終于找到 是item的問題。
那下邊就開始把item里的東西一點一點的隱藏看看。
如下圖,就一個相對布局,一個textview,我把相對布局invisible,完事發(fā)現(xiàn)問題還在,奇怪。
我另一個item本來復制的是這個,我這次就仿這個里邊就寫個textview,高度也一樣,結(jié)果發(fā)下那邊的沒問題啊,然后我對比了一下,兩個好像就差了個android:textIsSelectable="true",我就把這個加上,結(jié)果發(fā)現(xiàn)那邊的也自動滾上去了,至此終于發(fā)現(xiàn)問題根源了。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:id="@+id/layout_float_top"
android:layout_width="match_parent"
android:layout_height="150dp">
//省略
</RelativeLayout>
<TextView
android:id="@+id/tv_placeholder"
android:text="place holder"
android:gravity="center"
android:background="#ffffff"
android:textIsSelectable="true"
android:layout_width="match_parent"
android:layout_height="530dp" />
</LinearLayout>
測試
發(fā)現(xiàn)問題根源,測試的時候我就仔細看了下,我發(fā)現(xiàn)它自動滾上去的地方就是textview的底部,也就是本來正常狀態(tài)textview是有一部分在屏幕外邊看不見的,它會自動往上滾使textview完全可見。
然后我把textview高度改小,使得不滾動也能全部可見,果然它就不會自動滾動了。
完事我又想了下,那我textview的高度比屏幕高度還大的情況下,會是啥效果?
測試結(jié)果,這次是textview的頂部滾動動屏幕最上方為止。
結(jié)論
加上android:textIsSelectable="true"以后,如果textview的高度比屏幕高度還大,那么會讓textview的top滾動到屏幕的頂部,
如果textview的高度沒有屏幕高度大【而且textview有一部分不可見,在屏幕外邊】,那么會讓textview的bottomo滾動到屏幕的底部以使得textview完全可見。
其他人會碰到這問題嗎
感覺不太會,一般人也不會沒事弄個這么高的textview。我是測試用的,替代一堆復雜的布局,占個位置才這樣的,而又為了順手測試下文字選中功能,才加上這個。
如果真有人也碰到過,可以加個好友,幾率這么低,難兄難弟以后多交流。
原因
感覺是設置了selectable以后,會進行foucus的處理,而這里會進行invalidate進行l(wèi)ayout的
看寫這篇帖子,感覺寫的不錯,原理和scrollview其實是樣的
https://www.imooc.com/article/23067
看上邊文章就行,下邊只是跟著文章走一遍記錄一下而已。
第6點,測試發(fā)現(xiàn)是無效的,難道姿勢不對?
看下方法,確實的,設置了selectable以后,textview就會獲取焦點的
public void setTextIsSelectable(boolean selectable) {
if (!selectable && mEditor == null) return; // false is default value with no edit data
setFocusableInTouchMode(selectable);
setFocusable(FOCUSABLE_AUTO);
setClickable(selectable);
setLongClickable(selectable);
}
布局的加載過程,是從根root view開始一個一個add子child的
看下ViewGroup的addView方法
public void addView(View child, int index, LayoutParams params) {
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
繼續(xù)
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
final boolean childHasFocus = child.hasFocus();
if (childHasFocus) {
requestChildFocus(child, child.findFocus());
}
if (child.hasDefaultFocus()) {
// When adding a child that contains default focus, either during inflation or while
// manually assembling the hierarchy, update the ancestor default-focus chain.
setDefaultFocus(child);
}
}
go on
public void requestChildFocus(View child, View focused) {
//設置了FOCUS_BLOCK_DESCENDANTS的屬性,那么就被中斷了,不會往上傳遞了,所以如果我們設置了focus的view的任何父類添加了這個屬性,就不會傳遞給scrollview了。
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
// Unfocus us, if necessary
super.unFocus(focused);
// We had a previous notion of who had focus. Clear it.
if (mFocused != child) {
if (mFocused != null) {
mFocused.unFocus(focused);
}
//這里也能看到,如果add多個focus的child的話,最后的位置其實是最后一個child的位置。
mFocused = child;
}
if (mParent != null) {
mParent.requestChildFocus(this, focused);
}
}
可以看到代碼結(jié)尾又調(diào)用了parent的同名方法,也就是一層一層往上走了,直到中斷
如果沒有重寫這個方法,那么方法還是執(zhí)行上邊的代碼
不過ScrollView,RecyclerView都重寫了這個方法的,
ScrollView如下,可以看到對child進行了scroll操作。
public void requestChildFocus(View child, View focused) {
if (focused != null && focused.getRevealOnFocusHint()) {
if (!mIsLayoutDirty) {//這個boolean值標記的是布局有沒有完成,完成的話就是false
scrollToChild(focused);
} else {
// The child may not be laid out yet, we can't compute the scroll yet
mChildToScrollTo = focused;
}
}
//super方法[處理FOCUS_BLOCK_DESCENDANTS情況]是在上邊滾動處理之后調(diào)用的,
//所以給ScrollView設置android:descendantFocusability="blocksDescendants"屬性是無效的
super.requestChildFocus(child, focused);
}
下邊的方法處理了上邊的else情況。
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mIsLayoutDirty = false;
// Give a child focus if it needs it
if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
scrollToChild(mChildToScrollTo);
}
mChildToScrollTo = null;
}
最后看下view的幾個focus方法
public boolean isFocused() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}
public boolean hasFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}
public boolean hasFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0 || mFocused != null;
}
ViewGroup的,先看自己是不是,是就返回自己,不是就往下找,mFocused 這個變量上邊代碼有賦值
public View findFocus() {
if (isFocused()) {
return this;
}
if (mFocused != null) {
return mFocused.findFocus();
}
return null;
}
我們看下上邊分析的viewgroup的方法
public void requestChildFocus(View child, View focused) {
// Unfocus us, if necessary
super.unFocus(focused);//child有焦點的話,會clear掉自己的focus標志
// We had a previous notion of who had focus. Clear it.
if (mFocused != child) {
if (mFocused != null) {
mFocused.unFocus(focused);//添加了新的focused view,上一個focused view會被清除focus標志
}
mFocused = child;
}
}
看下unFocus
public void clearFocus() {
clearFocusInternal(null, true, true);
}
//看下if條件即可,可以發(fā)現(xiàn),如果自身是focused的話,會執(zhí)行非操作,也就是把focused屬性給去掉了
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
mPrivateFlags &= ~PFLAG_FOCUSED;
if (propagate && mParent != null) {
mParent.clearChildFocus(this);
}
onFocusChanged(false, 0, null);
refreshDrawableState();
if (propagate && (!refocus || !rootViewRequestFocus())) {
notifyGlobalFocusCleared(this);
}
}
}
早上寫的瀏覽器關閉沒了,懶得寫了,看下日志,可以發(fā)現(xiàn)childFocus的調(diào)用并不是在addView里的,而且日志里打印的hasFouce都是false,可以看到是在onLayout之后操作的,而且先調(diào)用的是onFocus方法,從root view一層一層的往下
I: LinearLayoutTemp:addView start=========android.support.v7.widget.AppCompatTextView{5168538 V.ED..... ......ID 0,0-0,0}===false====null
I: LinearLayoutTemp:addView end=========android.support.v7.widget.AppCompatTextView{5168538 V.ED..... ......ID 0,0-0,0}===false====null
I: LinearLayoutTemp:addView start=========com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. ......ID 0,0-0,0 #7f0a024b app:id/tv_test}===false====null
I: LinearLayoutTemp:addView end=========com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. ......ID 0,0-0,0 #7f0a024b app:id/tv_test}===false====null
I: LinearLayoutTemp:addView start=========android.support.v7.widget.AppCompatTextView{c68c350 VFED..CL. ......ID 0,0-0,0 #7f0a024a app:id/tv_temp}===false====null
I: LinearLayoutTemp:addView end=========android.support.v7.widget.AppCompatTextView{c68c350 VFED..CL. ......ID 0,0-0,0 #7f0a024a app:id/tv_temp}===false====null
I: touch slop===========16
I: result0==============false/false======false==false
I: ScrollViewTemp:refreshDrawableState==========================
I: ScrollViewTemp:onMeasure===========start
I: LinearLayoutTemp:onMeasure===========start
I: LinearLayoutTemp:onMeasure==============end
I: ScrollViewTemp:onMeasure==============end
I: ScrollViewTemp:onMeasure===========start
I: LinearLayoutTemp:onMeasure===========start
I: LinearLayoutTemp:onMeasure==============end
I: ScrollViewTemp:onMeasure==============end
I: ScrollViewTemp:onLayout===========start
I: LinearLayoutTemp:onLayout===========start
I: LinearLayoutTemp:onLayout==============end
I: ScrollViewTemp:onLayout==============end
I: 0/4==========0/144
I: 5/9==========144/270
I: 10/13==========270/349
I: ScrollViewTemp:requestFocus start=======262144
I: ScrollViewTemp:onRequestFocusInDescendants start=======direction:2==null
I: ScrollViewTemp:onRequestFocusInDescendants=============com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. ......ID 0,30-750,596 #7f0a024b app:id/tv_test}
I: LRTextView:requestFocus start=======
I: LinearLayoutTemp:requestChildFocus==start=====com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. .F....ID 0,30-750,596 #7f0a024b app:id/tv_test}===========com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. .F....ID 0,30-750,596 #7f0a024b app:id/tv_test}
I: ScrollViewTemp:requestChildFocus==start=====com.charliesong.demo0327.reader.LinearLayoutTemp{c9829b0 V.E...... ......ID 0,0-768,655 #7f0a0132 app:id/layout1}===========com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. .F....ID 0,30-750,596 #7f0a024b app:id/tv_test}
I: ScrollViewTemp:requestChildFocus==end=====com.charliesong.demo0327.reader.LinearLayoutTemp{c9829b0 V.E...... ......ID 0,0-768,655 #7f0a0132 app:id/layout1}===========com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. .F....ID 0,30-750,596 #7f0a024b app:id/tv_test}
I: LinearLayoutTemp:requestChildFocus==end=====com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. .F....ID 0,30-750,596 #7f0a024b app:id/tv_test}===========com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. .F....ID 0,30-750,596 #7f0a024b app:id/tv_test}
I: ScrollViewTemp:onRequestFocusInDescendants end=======true
I: ScrollViewTemp:draw===========start
I: ScrollViewTemp:onDraw===========start
I: ScrollViewTemp:onDraw==============end
I: ScrollViewTemp:dispatchDraw===========start
I: LinearLayoutTemp:dispatchDraw===========start
I: onDraw===========16=========16
I: LinearLayoutTemp:dispatchDraw==============end
I: ScrollViewTemp:dispatchDraw==============end
I: ScrollViewTemp:draw==============end
看下ScrollView
override fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean {
println("${javaClass.simpleName}:requestFocus start=======${descendantFocusability}")
return super.requestFocus(direction, previouslyFocusedRect)
}
//super的方法如下,因為我們沒有設置,所以默認走的是第三個FOCUS_AFTER_DESCENDANTS
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
int descendantFocusability = getDescendantFocusability();
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
return super.requestFocus(direction, previouslyFocusedRect);
case FOCUS_BEFORE_DESCENDANTS:{}
case FOCUS_AFTER_DESCENDANTS: {
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
return took ? took : super.requestFocus(direction, previouslyFocusedRect);
}
default:
}
}
然后ScrollView重寫的如下的方法,這里會找到第一個設置了focus的view,我們例子里的LRTextView
protected boolean onRequestFocusInDescendants(int direction,
Rect previouslyFocusedRect) {
// convert from forward / backward notation to up / down / left / right
// (ugh).
if (direction == View.FOCUS_FORWARD) {
direction = View.FOCUS_DOWN;
} else if (direction == View.FOCUS_BACKWARD) {
direction = View.FOCUS_UP;
}
final View nextFocus = previouslyFocusedRect == null ?
FocusFinder.getInstance().findNextFocus(this, null, direction) :
FocusFinder.getInstance().findNextFocusFromRect(this,
previouslyFocusedRect, direction);
if (nextFocus == null) {
return false;
}
if (isOffScreen(nextFocus)) {
return false;
}
return nextFocus.requestFocus(direction, previouslyFocusedRect);
}
如何找到這個view后邊再分析,先看下最后一行代碼干啥了
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
return requestFocusNoSearch(direction, previouslyFocusedRect);
}
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
//省略
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
//下邊方法就找到了我們要的requestChildFocus,然后一層一層往上走,和我們的日志一樣
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
mPrivateFlags |= PFLAG_FOCUSED;
if (mParent != null) {
mParent.requestChildFocus(this, this);//這里調(diào)用的
updateFocusedInCluster(oldFocus, direction);
}
//省略
}
}
現(xiàn)在分析下咋找到那個focus的view的
final View nextFocus = previouslyFocusedRect == null ?
FocusFinder.getInstance().findNextFocus(this, null, direction) :
FocusFinder.getInstance().findNextFocusFromRect(this,
previouslyFocusedRect, direction);
//首次加載布局的時候,那個rect是空的,所以走的findNextFocus,direction = View.FOCUS_DOWN
public final View findNextFocus(ViewGroup root, View focused, int direction) {
return findNextFocus(root, focused, null, direction);
}
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
View next = null;
ViewGroup effectiveRoot = getEffectiveRoot(root, focused);//focused為空,返回的還是root
if (focused != null) {
next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
}
if (next != null) {
return next;
}
ArrayList<View> focusables = mTempList;
try {
focusables.clear();
effectiveRoot.addFocusables(focusables, direction);
//這個看viewgroup里的方法,就是找到所有focus的View添加進來,我們例子中有2個,
//具體實現(xiàn)在ViewGroup方法里同名方法,如果child是ViewGroup,會繼續(xù)調(diào)用同名方法,添加它的子view
if (!focusables.isEmpty()) {//最后執(zhí)行的是這句
next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
}
} finally {
focusables.clear();
}
return next;
}
繼續(xù)上邊的findNextFocus方法,第二個第三個參數(shù)都是null
private View findNextFocus(ViewGroup root, View focused, Rect focused,
int direction, ArrayList<View> focusables) {
if (focused != null) {
} else {
if (focusedRect == null) {
focusedRect = mFocusedRect;
// make up a rect at top left or bottom right of root
switch (direction) {
case View.FOCUS_RIGHT:
case View.FOCUS_DOWN://是這個
setFocusTopLeft(root, focusedRect);
break;
}
}
}
}
switch (direction) {
case View.FOCUS_UP:
case View.FOCUS_DOWN:
case View.FOCUS_LEFT:
case View.FOCUS_RIGHT:
return findNextFocusInAbsoluteDirection(focusables, root, focused,
focusedRect, direction);
default:
throw new IllegalArgumentException("Unknown direction: " + direction);
}
}
//繼續(xù)往下。。。。。。。。。。。。。。。
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
Rect focusedRect, int direction) {
mBestCandidateRect.set(focusedRect);
switch(direction) {
case View.FOCUS_DOWN:
mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
}
View closest = null;
int numFocusables = focusables.size();
for (int i = 0; i < numFocusables; i++) {
View focusable = focusables.get(i);//
// only interested in other non-root views
if (focusable == focused || focusable == root) continue;
// get focus bounds of other view in same coordinate system
focusable.getFocusedRect(mOtherRect);//獲取的就是view的布局rect
root.offsetDescendantRectToMyCoords(focusable, mOtherRect);//
//下邊的if里方法條件判斷太復雜了,看的腦袋大,就不研究了。
if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
//找到第一個就把rect賦值給mBestCandidateRect,參與下一個view的比較,看誰更合適。
mBestCandidateRect.set(mOtherRect);
closest = focusable;
}
}
return closest;
}
判斷誰更靠譜好像最后應該走的這里,我們例子是FOCUS_DOWN,換句話說,看誰的top更接小誰就更靠譜
static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
switch (direction) {
case View.FOCUS_LEFT:
return source.left - dest.right;
case View.FOCUS_RIGHT:
return dest.left - source.right;
case View.FOCUS_UP:
return source.top - dest.bottom;
case View.FOCUS_DOWN:
return dest.top - source.bottom;
}
throw new IllegalArgumentException("direction must be one of "
+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
}
簡單結(jié)論
從最底層的root view開始執(zhí)行requestFocus
ViewGroup,分了3種情況,第三種是默認的
第一種,會執(zhí)行super,也就是view的同名方法,換句話說就是直接判斷自己是否可以獲取焦點
第二種,先super,就是先判斷自己能不能獲取焦點,能那就自己處理,不能再往下找
第三種,相反,先找最里邊的。
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
int descendantFocusability = getDescendantFocusability();
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
return super.requestFocus(direction, previouslyFocusedRect);
case FOCUS_BEFORE_DESCENDANTS: {
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
case FOCUS_AFTER_DESCENDANTS: {
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
return took ? took : super.requestFocus(direction, previouslyFocusedRect);
}
default:
}
}
看下第三種默認的,ViewGroup的,默認就是一層一層的去找的,而ScrollView是重寫了這個方法的,上邊有分析
protected boolean onRequestFocusInDescendants(int direction,
Rect previouslyFocusedRect) {
int index;
int increment;
int end;
int count = mChildrenCount;
if ((direction & FOCUS_FORWARD) != 0) {
index = 0;
increment = 1;
end = count;
} else {
index = count - 1;
increment = -1;
end = -1;
}
final View[] children = mChildren;
for (int i = index; i != end; i += increment) {
View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
if (child.requestFocus(direction, previouslyFocusedRect)) {
return true;
}
}
}
return false;
}
其他
RecyclerView滾動是一樣的道理,最后能看到scroll的字段的
public void requestChildFocus(View child, View focused) {
if (!mLayout.onRequestChildFocus(this, mState, child, focused) && focused != null) {
requestChildOnScreen(child, focused);
}
super.requestChildFocus(child, focused);
}
private void requestChildOnScreen(@NonNull View child, @Nullable View focused) {
mLayout.requestChildRectangleOnScreen(this, child, mTempRect, !mFirstLayoutComplete,
(focused == null));
}
public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect,
boolean immediate,
boolean focusedChildVisible) {
int[] scrollAmount = getChildRectangleOnScreenScrollAmount(parent, child, rect,
immediate);
int dx = scrollAmount[0];
int dy = scrollAmount[1];
if (!focusedChildVisible || isFocusedChildVisibleAfterScrolling(parent, dx, dy)) {
if (dx != 0 || dy != 0) {
if (immediate) {
parent.scrollBy(dx, dy);
} else {
parent.smoothScrollBy(dx, dy);
}
return true;
}
}
return false;
}
RecyclerView是一樣的道理,解決辦法也都一樣,在這個view的子view里添加
android:descendantFocusability="blocksDescendants"
ListView看了下,并沒有處理requestChildFocus,所以沒有這個問題。