要准确的理解这篇文章,首先需要理解Android KeyEvent分发机制
FocusViewGroup继承FrameLayout,重写dispatchKeyEvent()方法,如果当前区块内有控件获取焦点,处理KeyCode为300、301的事件。
区块内顺时针查找,是控件在布局内的排列顺序;反之,逆时针查找为排列的你顺序。
FocusViewGroup的实现如下:
public class FocusViewGroup extends FrameLayout {
private final static String TAG = "FocusViewGroup";
public FocusViewGroup(@NonNull Context context) {
super(context);
}
public FocusViewGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
boolean handled=super.dispatchKeyEvent(event);
if(!handled){
View next;
View directFocused=findFocus();
if(directFocused !=null&&event.getAction()==KeyEvent.ACTION_DOWN){
switch (event.getKeyCode()) {
case 300:
next=findNextFocused(directFocused);
if(next!=null){
handled=next.requestFocus();
}
break;
case 301:
next=findBeforeFocused(directFocused);
if(next!=null){
handled=next.requestFocus();
}
break;
default:
handled=super.dispatchKeyEvent(event);
break;
}
}else {
handled=super.dispatchKeyEvent(event);
}
}
return handled;
}
/**
*
* @param focused 当前焦点View
* @return 下一个获取焦点View
*/
private View findNextFocused(View focused) {
View next=null;
ArrayList<View> views=new ArrayList<>();
addFocusables(views,FOCUS_RIGHT);
for(int i=0;i<views.size();i++){
if(views.get(i)==focused){
next=views.get((i+1)%views.size());
break;
}
}
return next;
}
/**
** @param focused 当前焦点View
* @return 前一个获取焦点View
*/
private View findBeforeFocused(View focused){
View next=null;
ArrayList<View> views=new ArrayList<>();
addFocusables(views,FOCUS_LEFT);
for(int i=0;i<views.size();i++){
if(views.get(i)==focused){
next=views.get((i-1+views.size())%views.size());
break;
}
}
return next;
}
}
BigFocusViewGroup继承FrameLayout,BigFocusViewGroup的作用是识别上、下、左、右事件,并且根据方向找到下一个区块。
查找区块的方案参考FocusFinder,但是与FocusFinder并不完全一样,findNextFocusInAbsoluteDirection()方法的第一个参数需要的是FocusViewGroup的集合。findNextFocusInAbsoluteDirection()为private方法,无法重写,所以,复制FocusFinder的部分代码,findNextFocusInAbsoluteDirection()方法的传入参数。
获取FocusViewGroup集合的方法如下:
private ArrayList<View> findCandidates(ViewGroup root){
ArrayList<View> candidates = new ArrayList<>();
Queue<View> queue = new LinkedList<>();
View child;
for (int i = 0; i < root.getChildCount(); i++) {
child = root.getChildAt(i);
if (child instanceof ViewGroup) {
queue.add(root.getChildAt(i));
if (child instanceof FocusViewGroup) {
candidates.add(child);
}
}
while (!queue.isEmpty()) {
ViewGroup viewGroup = (ViewGroup) queue.poll();
for (int j = 0; j < viewGroup.getChildCount(); j++) {
child = viewGroup.getChildAt(j);
if (child instanceof ViewGroup) {
queue.add(child);
}
if (child instanceof FocusViewGroup) {
candidates.add(child);
}
}
}
}
return candidates;
}
ps:实现类似于图的广度优先遍历
BigFocusViewGroup定义一个成员变量mFocused保存当前获取焦点区块。mFocused的赋值很有趣。
当View调用requestFocus()方法时,会调用父容器的requestChildFocus()方法,层层向上,通知上层容器获取焦点child是哪一个和实际获取焦点的控件是哪一个。
根据上面焦点控件向上传递的机制,可以重写BigFocusViewGroup的requestChildFocus()方法:
@Override
public void requestChildFocus(View child, View focused) {
super.requestChildFocus(child, focused);
ViewParent parent = focused.getParent();
while (parent != null) {
if (parent instanceof FocusViewGroup) {
mFocused = (ViewGroup) parent;
break;
}
parent = parent.getParent();
}
}
BigFocusViewGroup重写dispatchKeyEvent()方法处理上、下、左、右事件:
public boolean dispatchKeyEvent(KeyEvent event) {
boolean handled = false;
View next;
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_UP:
next = FocusViewGroupFinder.getInstance().findNextFocus(this, mFocused, FOCUS_UP);
if (next != null) {
next.requestFocus();
}
handled = true;
break;
......
}
} else {
handled = super.dispatchKeyEvent(event);
}
return handled;
}
继承ListView重写onKeyDown()方法,当ListView里有已获取焦点控件时处理KeyCode为300和301的事件:
public boolean onKeyDown(int keyCode, KeyEvent event) {
boolean result = false;
if (getFocusedChild() != null && event.getAction() == KeyEvent.ACTION_DOWN) {
result = true;
switch (event.getKeyCode()) {
case 300:
requestNextFocus();
break;
case 301:
requestBeforeFocused();
break;
default:
break;
}
}
return result;
}
以KeyCode=300为例,查找下一个可获取焦点控件。在item里按控件顺序查找下一个可获取焦点控件,找到则控件获取焦点,没找到则下一个item第一个可获取焦点控件获取焦点。代码如下:
private void requestNextFocus() {
View next;
View focusedChild = getFocusedChild();
View focused = focusedChild.findFocus();
ArrayList<View> focusableList = findFocusableInChild(focusedChild,FOCUS_RIGHT);
//现在child下查找下一个获取焦点控件
for (int i = 0; i < focusableList.size(); i++) {
if (focusableList.get(i) == focused && (i + 1) < focusableList.size()) {
next = focusableList.get(i + 1);
next.requestFocus();
//View不完全显示滚动知道完全显示
scrollToNext(getPositionForView(focusedChild), next,FOCUS_DOWN);
return;
}
}
//在child中未找到下一个焦点控件,焦点移动到下一个child
View nextFocusedChild;
int position = getPositionForView(focusedChild);
if (position + 1 >= getCount()) {
return;
}
if ((position + 1 - getFirstVisiblePosition()) < getChildCount()) {
nextFocusedChild = getChildAt(position + 1 - getFirstVisiblePosition());
//要处理控件是否在屏幕上情况,如果不在,需要滚动ListView
nextFocusedChild.requestFocus();
scrollToNext(position + 1, nextFocusedChild.findFocus(),FOCUS_DOWN);
} else {
scrollToNext(position+1,null,FOCUS_DOWN);
}
}
虽然下一个控件获取了焦点,但是还要考虑控件是否完全显示在屏幕上,如果没有完全显示,要滚动ListView使控件完全显示出来。代码如下:
private void scrollToNext(final int position, View nextFocused,int direction) {
//直接向上或向下滚动ListView一个child
if(nextFocused==null){
switch (direction){
case FOCUS_UP:
if(position>=0){
smoothScrollToPosition(position);
postDelayed(new Runnable() {
@Override
public void run() {
View focusedChild=getChildAt(position-getFirstVisiblePosition());
if(focusedChild!=null){
ArrayList<View> focusableList=findFocusableInChild(focusedChild,FOCUS_LEFT);
focusableList.get(focusableList.size()-1).requestFocus();
}
}
},200);
}
break;
case FOCUS_DOWN:
if(position<getCount()){
smoothScrollToPosition(position);
/*smoothScrollToPosition在UI线程执行,需要时间SCROLL_DURATION / viewTravelCount详情查看AbsListView*/
postDelayed(new Runnable() {
@Override
public void run() {
View focusedChild=getChildAt(position-getFirstVisiblePosition());
if(focusedChild!=null){
focusedChild.requestFocus();
}
}
},200);
}
break;
default:
break;
}
return;
}
Rect focusedRect = new Rect();
nextFocused.getFocusedRect(focusedRect);
//focused平移到ListView坐标系下
offsetDescendantRectToMyCoords(nextFocused, focusedRect);
//top是否超过ListView顶部
switch (direction){
case FOCUS_UP:
if(focusedRect.top < 0){
smoothScrollBy(focusedRect.top,0);
}
break;
case FOCUS_DOWN:
//判断focused的bottom是否超过ListView的底部
if(focusedRect.bottom > getMeasuredHeight() - getListPaddingBottom()){
//不能用scrollBy,因为ListView里的View没有更新坐标(left,top,right,bottom),下一次依旧会滚动
smoothScrollBy(focusedRect.bottom-getMeasuredHeight()-getListPaddingBottom(),0);
}
break;
default:
break;
}
}