一、概述
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
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:fillEnabled="true">
<scale
android:duration="100"
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" />
</set>
app\src\main\res\anim\view_scale_big.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:fillEnabled="true">
<scale
android:duration="100"
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" />
</set>
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
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="video_vod_my_playlist_pos" type="id" />
</resources>
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
* <p>
* 提供:
* 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<Integer> 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<Integer> 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;
}
}