一、概述
1.无论在任何界面都只会有一个view获得焦点,焦点调试一般是用在tv类的设备上操作比较多,一般都是遥控器操作
2.焦点移动规则介绍
1).一般焦点的规则是最开始界面初始化的时候是会去遍历view容器里面哪些view有焦点属性
2).如果有控件有焦点属性的话,就会根据从左右到,从上到下的规则去寻找焦点属性的控件,同一行控件,左边的控件优先获得焦点,同一列控件,上面的控件优先获得焦点,
3).如果按了上下左右操作时,控件的查找规则也类似,当down按下时会去周围查找具有焦点属性的控件,如果没找到就下一个有焦点属性的控件的话就还是自己拿焦点(recycleview快速移动时除外,后面会介绍),
4).当找到控件时,就会再up的时候在那个view获得焦点,如果此时在down的时候强制让某个view请求焦点(requestFocus(),并return true)的话,就可以修改系统原生的焦点查找规则,从而达到客制化焦点移动的效果,如果down的时候不让某个view强制获得了焦点,但是此时没有return true的话,则焦点还是会按系统原始的规则去查找焦点控件
3.有些控件如Butoon等是默认就有焦点属性,一般的Relativelayout等布局以及Imageview等控件是没有焦点属性的
二、常用接口介绍
1.代码类
view.setFocusable(true);//设置控件可以获得焦点属性
view.setFocusableInTouchMode(true);//设置控件可以获得焦点触摸属性
view.isFocusable();//判断控件是否能获得焦点
view.hasFocus();//判断控件是否有焦点
view.requestFocus();//控件强制请求焦点
2.xml属性类
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusDown="@+id/view_id" 定义控件上下左右操作后哪个控件获得焦点
android:descendantFocusability="xxx"
xxx的取值有3个:beforeDescendants、afterDescendants、blocksDescendants(一般是listview和gridview中用的多点,recycleview不需要)
beforeDescendants:父控件会优先其子类控件而获取到焦点;
afterDescendants:父控件只有当其子类控件不需要获取焦点时才获取焦点
blocksDescendants:父控件会覆盖子类控件而直接获得焦点;
3.focusableInTouchMode如果有触摸操作时,控件如果开始没焦点,第一时间是会先获得焦点,而不是执行点击操作,当控件获得焦点后才会执行点击操作
三、焦点常见应用场景
1.正常情况下view的效果有3种,一种是选中,一种是上焦点,一种是常态,常见的状态是上焦点时view放大,有个方框,没焦点的时候view缩小无方框,有的还需要修改文本的颜色等效果,基本原理都是通过view的onFocusChange去实现的,view有焦点时hasfocus为true,无焦点返回false
@Override
public void onBindViewHolder(final MyViewHolder holder,final int position) {
holder.getBinding().rlMovieDetailRvItem.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if(hasFocus){
Animation animation = AnimationUtils.loadAnimation(mContext, R.anim.view_scale_big);//放大
holder.getBinding().ivActionExplanation.startAnimation(animation);
holder.getBinding().rlMovieDetailRvItem.setBackgroundResource (R.drawable.playcontrol_moviedetail_item_bg_yellow_focus);
//有的时候还需要修改字体的颜色,原理一样
}else{
Animation animation = AnimationUtils.loadAnimation(mContext, R.anim.view_scale_small);//缩小
holder.getBinding().ivActionExplanation.startAnimation(animation);
holder.getBinding().rlMovieDetailRvItem.setBackgroundResource (R.drawable.playcontrol_moviedetail_item_bg_no_focus);
}
}
});
app\src\main\res\anim\view_scale_small.xml
android:fillEnabled="true">
android:fromXScale="1.07"
android:fromYScale="1.07"
android:interpolator="@android:anim/linear_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1.0"
android:toYScale="1.0" />
app\src\main\res\anim\view_scale_big.xml
android:fillEnabled="true">
android:fromXScale="1.0"
android:fromYScale="1.0"
android:interpolator="@android:anim/linear_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1.07"
android:toYScale="1.07" />
2.显示内容较多时的列表显示场景
根据个人经验俩说焦点界面里面使用的最多并且也建议用的列表类的view就是recycleview,因为listview和gridview使用的时候可能会有很多坑或者使用不方便的情况,比如需要设置子容器优先获得焦点,去掉listview/gridview本身的选中效果等
3.焦点调节中tag的使用:
原理就是将view通过tag绑定一个属性值,这个属性值可以是int,也可以是String等其他对象,然后可以在其他地方通过这个view的id去获得其tag的属性,常见的就是从recycleview的列表项中通过得到itemview然后得到itemview的位置,开始是在onBindViewHolder时通过itemview绑定position,之后可以再在activity或view的dispatchKeyEvent中通过getCurrentFocus()获得当前获得焦点的view,如果是recycleview的item获得焦点,则此时通过此itemview就可以通过tag获得此itemview的position;类似的也可以通过tag去存储imageview所需要的url,减小直接定义bean的代码
使用方法如下:
1).rv的adapter中的item设置绑定位置的tag
@Override
public void onBindViewHolder(final MyViewHolder holder,final int position) {
...
myHolder.rl.setTag(R.id.video_vod_my_playlist_pos,position);
2).创建attr.xml文件添加id
app\src\main\res\values\attr.xml
3).通过id获得绑定tag的值
view为recycleview的item的view
int position = (int) view.getTag(R.id.video_vod_my_playlist_pos);
4.常见效果及实现方式
1).居中效果:左右/上下移动居中-建议直接用RecycleviewTV类,已经自带此效果设置,recyleview中间居中的原理
@Override
public void requestChildFocus(View child, View focused) {
super.requestChildFocus(child, focused);
if (selectView != null) {
selectView.setId(View.NO_ID);
}
if (null != focused) {
selectView = child;
selectView.setId(mLastSelectedViewId);
// ==if 居中逻辑
if (mSelectedItemCentered) {//是否居中
if (!isVertical()) {
int dx = (int) (focused.getX() - (getWidth() / 2)) + (focused.getWidth() / 2);
smoothScrollBy(dx, 0);
} else {
int dy = (int) (focused.getY() - (getHeight() / 2)) + (focused.getHeight() / 2);
smoothScrollBy(0, dy);
}
}
}
}
2).设置recycleview的某个item获得焦点-RecycleviewTV-setSelect
public void setSelect(final int position) {
this.post(new Runnable() {
@Override
public void run() {
if (getLayoutManager() != null) {
View view = getLayoutManager().findViewByPosition(position);
if (view != null) {
view.requestFocus();
} else {
view = getLayoutManager().findViewByPosition(position - 1);
if (view != null) {
view.requestFocus();
}
}
}
}
});
}
3).边界处理效果:一般的tv焦点产品都会有边界处理,就是上下左右到了边界不能移动时需要有个动画效果提醒。如果是列表view,则一般通过tag获得item的位置去判断边界,如果是简单的布局则通过判断边界view的id去判断,如果是边界控件id则执行一个动画效果
四、调试经验
1.调试时一般通过public boolean dispatchKeyEvent(KeyEvent event) {}方法去处理焦点问题,常见的就是上/下/左/右/OK(Enter)/返回键的一些处理,其中在activity中是可以通过getCurrentFocus()获得当前焦点的view(如果是view容器类则是通过getFocusedChild()获得焦点view),调试时一般如果碰到不知道焦点跑哪去了,就可以打印下action_down时焦点在哪里,aciton_up时焦点在哪里,从而分析焦点是如何走的,
public boolean dispatchKeyEvent(KeyEvent event) {
int action = event.getAction();
int keyCode = event.getKeyCode();
if(action == KeyEvent.ACTION_DOWN){
Log.d(TAG, "down focusView="+getCurrentFocus());
switch (keyCode){
case KeyEvent.KEYCODE_DPAD_UP:
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
break;
case KeyEvent.KEYCODE_ENTER:
break;
case KeyEvent.KEYCODE_BACK:
break;
default:
break;
}
}else if(action == KeyEvent.ACTION_UP){
Log.d(TAG, "up focusView="+getCurrentFocus());
}
return super.dispatchKeyEvent(event);
}
2.recycleview快速移动焦点丢失问题
出现这种现象的原因是由于recycleview在加载item的时候需要时间,如果焦点快速移动时,此时itemview还没绘制出来就会出现焦点丢失的情况(焦点可能会跑到recycleview外面有焦点属性的控件上面去),这种情况一般是在recycleview的边界加一个透明的view,让快速移动的item还没绘制出来时焦点跑到这个透明的view上面去,当item绘制出来的时候item会自然的获得焦点,看到的视觉效果就是很顺滑的移动到下一个item,且没有丢失焦点
扩展:翻页时还是同样的列表显示,思路是只显示一页的item项,翻页时只更新item的内容
3.遥控器点击快进/快退时焦点跑到歌词上面去了,导致遥控器点击快进快退按钮无效
4.tvplayer调用某个接口,执行repeat多次,显示异常
device\mstar\common\apps\MTvPlayer\src\com\mstar\tv\tvplayer\ui\MainMenuActivity.java
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
int action = event.getAction();
int keyCode = event.getKeyCode();
if(action == KeyEvent.ACTION_DOWN){
View focusView = getCurrentFocus();
switch (keyCode){
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_LEFT:
if(focusView.getId()== R.id.linearlayout_hdmi_edid_version && event.getRepeatCount()>0){
return true;
}else if(focusView.getId()== R.id.linearlayout_pic_picturemode && event.getRepeatCount()>0){
return true;
}
break;
default:
break;
}
}
return super.dispatchKeyEvent(event);
}
5.holder.setIsRecyclable(false)--如果recycleview出现item重用显示有问题的时候可以用这个api解决。比如tcl多任务列表,为了达到效果,实现方式是左右边界加了一个空的view,只是不显示而已,但是左右移动如果重用了不显示的view就会显示异常
6.Scrowview多页显示时建议用大的recycleview去做,如果数据量大时,Scrowview会出现加载时间比较长的情况,原因是Scrowview会需要等每个内容都加载完毕之后才显示,而recycleview有重用回收机制,需要加载数据时才显示,此时加载显示就会快些
7.触摸点击和遥控器enter键效果不一样:触摸点击通过ontouch实现,enter还是走正常的点击事件
8.recycleview的item绘制顺序-getChildDrawingOrder。应用场景:tcl-多任务列表,item的放大时会覆盖左右两边的item,但是正常绘制顺序是从左到右,会导致中间的view会被最右边的view覆盖一部分,此时则需要手动修改item的绘制顺序,让中间的itemview最后绘制
9.recycleview的item尽量用相对布局(比如文件管理器的item是线性布局,拿不到焦点),尽量是item的父容器获得焦点,item如果是需要控制seekbar的除外(比如tcl的item-seekbar调节参数,可以让seekbar获得焦点,然后seerbar获得焦点的时候去控制父容器的显示),item的焦点处理都是通过itemview的setOnFocusChangeListener
============
import android.content.Context;
import android.support.annotation.IdRes;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.AttributeSet;
import android.view.FocusFinder;
import android.view.KeyEvent;
import android.view.View;
import com.mgtv.fitness.util.LogUtil;
import java.util.HashSet;
/**
* 1.设置是否选中居中;
* 2.上下左右边缘判断;
* 3.获取最后一次焦点的View 或者pos
* 4.设置焦点位置6
*
* 提供:
* 1.Item 点击事件回调 OnItemClickListener
* 2.Item 焦点事件回调 OnItemListener 已经位置定位onReviseFocusFollow
*/
public class RecyclerViewTV extends RecyclerView {
private static final String TAG = "RecyclerViewTV";
private
@IdRes
int mLastSelectedViewId = View.NO_ID;
private boolean isHandleSelectionWhenItemFocusChanged = true;
public RecyclerViewTV(Context context) {
this(context, null);
}
public RecyclerViewTV(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public RecyclerViewTV(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);//优先让子控件获取焦点
setHasFixedSize(true);
setWillNotDraw(true);
setOverScrollMode(View.OVER_SCROLL_NEVER);
setChildrenDrawingOrderEnabled(true);
setClipChildren(false);
setClipToPadding(false);
setClickable(false);
setFocusable(false);
setFocusableInTouchMode(false);
}
public void setLastSelectedViewId(@IdRes int id) {
mLastSelectedViewId = id;
}
public
@IdRes
int getLastSelectedViewId() {
return mLastSelectedViewId;
}
private boolean mSelectedItemCentered = true;//居中
private View selectView;//选中的一个View
@Override
public void onChildAttachedToWindow(View child) {
// LogUtils.d(TAG, child.getId() + ",位置=" + this.getChildAdapterPosition(child));
if (child != null) {
if (mOnItemClickListener != null) {
child.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View itemView) {
mOnItemClickListener.onItemClick(RecyclerViewTV.this, itemView, getChildLayoutPosition(itemView));
}
});
}
if (mOnItemListener != null) {
child.setOnFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(View itemView, boolean hasFocus) {
if (isHandleSelectionWhenItemFocusChanged) {
itemView.setSelected(hasFocus);
}
if (hasFocus) {
mOnItemListener.onItemSelected(RecyclerViewTV.this, itemView, getChildLayoutPosition(itemView));
} else {
mOnItemListener.onItemPreSelected(RecyclerViewTV.this, itemView, getChildLayoutPosition(itemView));
}
}
});
}
}
}
public void setHandleSelectionWhenItemFocusChanged(boolean yes) {
isHandleSelectionWhenItemFocusChanged = yes;
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (!isVertical()) {
if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT
&& event.getAction() == KeyEvent.ACTION_DOWN) {
View focusView = findFocus();
if (focusView == null) {
LogUtil.d( "未找到下一个焦点");
return true;
}
View nextFocusUpView = FocusFinder.getInstance()
.findNextFocus(this, focusView, View.FOCUS_LEFT);
if (nextFocusUpView == null) {
LogUtil.d("未找到下一个焦点");
return true;
}
} else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT
&& event.getAction() == KeyEvent.ACTION_DOWN) {
View focusView = findFocus();
if (focusView == null) {
LogUtil.d("未找到下一个焦点");
return true;
}
View nextFocusUpView = FocusFinder.getInstance()
.findNextFocus(this, focusView, View.FOCUS_RIGHT);
if (nextFocusUpView == null) {
LogUtil.d("未找到下一个焦点");
return true;
}
}
}
return super.dispatchKeyEvent(event);
}
@Override
public void requestChildFocus(View child, View focused) {
super.requestChildFocus(child, focused);
if (selectView != null) {
selectView.setId(View.NO_ID);
}
if (null != focused) {
selectView = child;
selectView.setId(mLastSelectedViewId);
// ==if 居中逻辑
if (mSelectedItemCentered) {//是否居中
if (!isVertical()) {
int dx = (int) (focused.getX() - (getWidth() / 2)) + (focused.getWidth() / 2);
smoothScrollBy(dx, 0);
} else {
int dy = (int) (focused.getY() - (getHeight() / 2)) + (focused.getHeight() / 2);
smoothScrollBy(0, dy);
}
}
}
}
@Override
public void onScrollStateChanged(int state) {
if (state == SCROLL_STATE_IDLE) {
final View focuse = getFocusedChild();
if (null != mOnItemListener && null != focuse) {
mOnItemListener.onReviseFocusFollow(this, focuse, getChildLayoutPosition(focuse));
}
}
super.onScrollStateChanged(state);
}
// @Override
// public View focusSearch(View focused, int direction) {
// View nextFocused = FocusFinder.getInstance().findNextFocus(this, focused,
// direction);
// if (nextFocused == null) {
// return focused;
// }
// return super.focusSearch(focused, direction);
// }
/**
* 判断是垂直,还是横向.
*/
protected boolean isVertical() {
if (getLayoutManager() instanceof StaggeredGridLayoutManager) {
StaggeredGridLayoutManager layout = (StaggeredGridLayoutManager) getLayoutManager();
return layout.getOrientation() == StaggeredGridLayoutManager.VERTICAL;
} else if (getLayoutManager() instanceof MyLayoutManager) {
LayoutManager layout = getLayoutManager();
return false;
} else {
LinearLayoutManager layout = (LinearLayoutManager) getLayoutManager();
return layout.getOrientation() == LinearLayoutManager.VERTICAL;
}
}
/**
* 通过View 索引 下标
*
* @param view
* @return
*/
private int getPositionByView(View view) {
if (view == null) {
return NO_POSITION;
}
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (params == null || params.isItemRemoved()) {
// when item is removed, the position value can be any value.
return NO_POSITION;
}
return params.getViewPosition();
}
//===============================公布的方法:
/**
* 是否 居中,默认居中 true
*
* @param mSelectedItemCentered
*/
public void setmSelectedItemCentered(boolean mSelectedItemCentered) {
this.mSelectedItemCentered = mSelectedItemCentered;
}
/**
* 选中的View
*
* @return
*/
public View getSelectView() {
if (selectView == null) {
selectView = getFocusedChild();
}
return selectView;
}
/**
* 选中View 的下标位置
*
* @return
*/
public int getSelectPosition() {
View view = getSelectView();
if (view != null) {
return getPositionByView(view);
}
return -1;
}
/**
* 是否在最顶上一排
*
* @return
*/
public boolean isOnFarTop() {
int selectPosition = this.getSelectPosition();
int spanCount = this.getSpanCount();
if (!isVertical() && (getLayoutManager() instanceof LinearLayoutManager)) {//水平 垂直线性
return true;
}
if (getLayoutManager() instanceof MyLayoutManager) {
MyLayoutManager layoutManager = (MyLayoutManager) getLayoutManager();
return layoutManager.getFristLinsPostion() > selectPosition;
}
return selectPosition < spanCount;
}
/**
* 是否在最左边
*
* @return
*/
public boolean isOnFarLeft() {
int selectPosition = this.getSelectPosition();
int spanCount = this.getSpanCount();
if (!isVertical() && (getLayoutManager() instanceof LinearLayoutManager)) {//水平 垂直线性
return selectPosition == 0;
}
if (getLayoutManager() instanceof MyLayoutManager) {
MyLayoutManager layoutManager = (MyLayoutManager) getLayoutManager();
HashSet leftPostions = layoutManager.getLeftPostions();
return leftPostions.contains(selectPosition);
}
return selectPosition % spanCount == 0;
}
/**
* 是否在最右边
*
* @return
*/
public boolean isOnFarRight() {
int selectPosition = this.getSelectPosition();
int spanCount = this.getSpanCount();
int totalItemCount = this.getLayoutManager().getItemCount();
if (!isVertical() && (getLayoutManager() instanceof LinearLayoutManager)) {//水平 垂直线性
return selectPosition == totalItemCount - 1;
}
if (getLayoutManager() instanceof MyLayoutManager) {
MyLayoutManager layoutManager = (MyLayoutManager) getLayoutManager();
HashSet rightPostions = layoutManager.getRightPostions();
return rightPostions.contains(selectPosition);
}
//最后一个
if (selectPosition == totalItemCount - 1) {
return true;
}
return (selectPosition + 1) % spanCount == 0;
}
/**
* 是否在最下面
*
* @return
*/
public boolean isOnFarBottom() {
int totalItemCount = this.getLayoutManager().getItemCount();
int selectPosition = this.getSelectPosition();
int spanCount = this.getSpanCount();
if (!isVertical() && (getLayoutManager() instanceof GridLayoutManager)) {
if (((selectPosition + 1) % spanCount == 0) || selectPosition + 1 == totalItemCount) {
return true;
} else {
return false;
}
}
if (!isVertical() && (getLayoutManager() instanceof LinearLayoutManager)) {//水平 垂直线性
return true;
}
if (getLayoutManager() instanceof MyLayoutManager) {
MyLayoutManager layoutManager = (MyLayoutManager) getLayoutManager();
int lastLinsPostion = layoutManager.getLastLinsPostion();
if (selectPosition >= lastLinsPostion) {
return true;
}
}
//总长度,-当前选中位置 小于一列数,即在最后一行
return totalItemCount - selectPosition <= spanCount;
}
/**
* @return 列数
*/
public int getSpanCount() {
if (getLayoutManager() instanceof GridLayoutManager) {
return ((GridLayoutManager) getLayoutManager()).getSpanCount();
} else if (getLayoutManager() instanceof StaggeredGridLayoutManager) {
return ((StaggeredGridLayoutManager) getLayoutManager()).getSpanCount();
}
return 1;
}
/**
* zero
* 设置选中
*
* @param position
*/
public void setSelect(final int position) {
this.post(new Runnable() {
@Override
public void run() {
if (getLayoutManager() != null) {
View view = getLayoutManager().findViewByPosition(position);
if (view != null) {
view.requestFocus();
} else {
view = getLayoutManager().findViewByPosition(position - 1);
if (view != null) {
view.requestFocus();
}
}
}
}
});
}
public int getItemCount() {
return getLayoutManager().getItemCount();
}
// ====== 回调接口
public OnItemClickListener mOnItemClickListener;
public interface OnItemClickListener {
void onItemClick(RecyclerViewTV parent, View itemView, int position);
}
public void setmOnItemClickListener(OnItemClickListener mOnItemClickListener) {
this.mOnItemClickListener = mOnItemClickListener;
}
public OnItemListener mOnItemListener;
public interface OnItemListener {
void onItemPreSelected(RecyclerViewTV parent, View itemView, int position);
void onItemSelected(RecyclerViewTV parent, View itemView, int position);
void onReviseFocusFollow(RecyclerViewTV parent, View itemView, int position);
}
public void setmOnItemListener(OnItemListener mOnItemListener) {
this.mOnItemListener = mOnItemListener;
}
//清楚选中状态
public void clearSelectState() {
selectView = null;
}
}