Android Tv 的交互是通过遥控器来进行的,焦点移动是用户操作后的直观感受。如何让用户直观的操控Tv是本文的重点介绍内容。
1. Tv开发一般都会有自定义的Launcher,launcher中显示媒体资源数据。类似下图
每个item选中后会有个边框,并且会变大。下面将如何实现这个效果。
放大效果
首先item的布局,以LinearLayout为例,我们要的效果是当LinearLayout或者Imageview 获取焦点时, LinearLayout放大,当焦点移动到其他item时,此item丢失焦点,恢复原来的大小。所以我们自定义LinearLayout。
public class CustomLinearLayout extends LinearLayout implements View.OnFocusChangeListener, CustomViewInterface, View.OnHoverListener {
CustomFocusedChanged focusedChanged;
public CustomLinearLayout(Context context) {
super(context);
init();
}
public CustomLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public CustomLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public void setFocusedChanged(CustomFocusedChanged focusedChanged) {
this.focusedChanged = focusedChanged;
}
@Override
public void init() {
this.setOnFocusChangeListener(this);
this.setOnHoverListener(this);
}
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (focusedChanged != null) {
focusedChanged.onFocusChange(v, hasFocus);//外部接口调用
}
DisplayUtil.setViewAnimator(v, hasFocus);//放大缩小动画
}
//鼠标事件处理
@Override
public boolean onHover(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
DisplayUtil.setViewAnimator(v, true);
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
DisplayUtil.setViewAnimator(v, false);
}
return false;
}
}
接口介绍
public interface CustomViewInterface {
void init();
void setFocusedChanged(CustomFocusedChanged focusedChanged);
}
此接口作用是因为我们的自定义控件获取焦点,当外部需要监听其焦点变化时,若再调用View.OnFocusChangeListener接口会覆盖掉我们自定义控件中的监听,所有再写一个接口供外部调用。
View.OnHoverListener接口未监听鼠标接口
实现onHover 方法
@Override
public boolean onHover(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
//鼠标进入控件范围
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
//鼠标离开控件
}
return false;
}
放大动画
public static final float ZOOM_SCALE = 1.05f;
private static final float ORIGIN_SCALE = 1.0f;
public static void setViewAnimator(View v, boolean focus) {
setViewAnimator(v, focus, ZOOM_SCALE);
}
public static void setViewAnimator(View v, boolean focus, float... params) {
AnimatorSet animatorSet = new AnimatorSet();//组合动画
ObjectAnimator scaleX;
ObjectAnimator scaleY;
if (focus) {
scaleX = ObjectAnimator.ofFloat(v, "scaleX", ORIGIN_SCALE, params[0]);
scaleY = ObjectAnimator.ofFloat(v, "scaleY", ORIGIN_SCALE, params[0]);
} else {
scaleX = ObjectAnimator.ofFloat(v, "scaleX", params[0], ORIGIN_SCALE);
scaleY = ObjectAnimator.ofFloat(v, "scaleY", params[0], ORIGIN_SCALE);
}
if (params.length > 1) {
v.setPivotX(params[1]);
v.setPivotY(params[2]);
}
animatorSet.setDuration(300);
animatorSet.setInterpolator(new DecelerateInterpolator());
animatorSet.play(scaleX).with(scaleY);//两个动画同时开始
animatorSet.start();
}
圆角效果
重点类:RoundDrawable,ColorStateList
public class RoundLinearLayout extends LinearLayout{
private ColorStateList mSolidColor;
private int mCornerRadius;
public RoundLinearLayout(Context context) {
super(context);
}
public RoundLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public RoundLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public RoundLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RoundButton);
float pressedRatio = a.getFloat(R.styleable.RoundButton_btnPressedRatio, 0.80f);
mCornerRadius = a.getLayoutDimension(R.styleable.RoundButton_btnCornerRadius, getResources().getDimensionPixelSize(R.dimen.radius_default));
mSolidColor = a.getColorStateList(R.styleable.RoundButton_btnSolidColor);
int strokeColor = a.getColor(R.styleable.RoundButton_btnStrokeColor, Color.GRAY);
int strokeWidth = a.getDimensionPixelSize(R.styleable.RoundButton_btnStrokeWidth, 0);
int strokeDashWidth = a.getDimensionPixelSize(R.styleable.RoundButton_btnStrokeDashWidth, 0);
int strokeDashGap = a.getDimensionPixelSize(R.styleable.RoundButton_btnStrokeDashGap, 0);
a.recycle();
RoundDrawable rd = new RoundDrawable(mCornerRadius == -1);
rd.setCornerRadius(mCornerRadius == -1 ? 0 : mCornerRadius);
rd.setStroke(strokeWidth, strokeColor, strokeDashWidth, strokeDashGap);
if (mSolidColor == null) {
mSolidColor = ColorStateList.valueOf(Color.GRAY);
}
if (mSolidColor.isStateful()) {
rd.setSolidColors(mSolidColor);
} else if (pressedRatio > 0.0001f) {
rd.setSolidColors(csl(mSolidColor.getDefaultColor(), pressedRatio));
} else {
rd.setColor(mSolidColor.getDefaultColor());
}
setBackground(rd);
}
public void setSolidColor(int color) {
RoundDrawable rd = new RoundDrawable(mCornerRadius == -1);
rd.setCornerRadius(mCornerRadius == -1 ? 0 : mCornerRadius);
mSolidColor = ColorStateList.valueOf(color);
if (mSolidColor.isStateful()) {
rd.setSolidColors(mSolidColor);
} else {
rd.setColor(mSolidColor.getDefaultColor());
}
setBackground(rd);
}
int darker(int color, float ratio) {
color = (color >> 24) == 0 ? 0x22808080 : color;
float[] hsv = new float[3];
Color.colorToHSV(color, hsv);
hsv[2] *= ratio;
return Color.HSVToColor(color >> 24, hsv);
}
ColorStateList csl(int normal, float ratio) {
// int disabled = greyer(normal);
int pressed = darker(normal, ratio);
int[][] states = new int[][]{{android.R.attr.state_pressed}, {}};
int[] colors = new int[]{pressed, normal};
return new ColorStateList(states, colors);
}
RoundDrawable
public class RoundDrawable extends GradientDrawable {
private boolean mIsStadium = false;
private ColorStateList mSolidColors;
private int mFillColor;
public RoundDrawable(boolean isStadium) {
mIsStadium = isStadium;
}
public void setSolidColors(ColorStateList colors) {
mSolidColors = colors;
setColor(colors.getDefaultColor());
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
if (mIsStadium) {
RectF rect = new RectF(getBounds());
setCornerRadius((rect.height() > rect.width() ? rect.width() : rect.height()) / 2);
}
}
@Override
public void setColor(int argb) {
mFillColor = argb;
super.setColor(argb);
}
@Override
protected boolean onStateChange(int[] stateSet) {
if (mSolidColors != null) {
final int newColor = mSolidColors.getColorForState(stateSet, 0);
if (mFillColor != newColor) {
setColor(newColor);
return true;
}
}
return false;
}
@Override
public boolean isStateful() {
return super.isStateful() || (mSolidColors != null && mSolidColors.isStateful());
}
}
attrs文件
<declare-styleable name="RoundButton">
<!-- 背景色 -->
<attr name="btnSolidColor" format="color"/>
<!-- 边框色 -->
<attr name="btnStrokeColor" format="color"/>
<!-- 边框厚度 -->
<attr name="btnStrokeWidth" format="dimension"/>
<!-- 边框虚线长度 -->
<attr name="btnStrokeDashWidth" format="dimension"/>
<!-- 边框虚线间隙 -->
<attr name="btnStrokeDashGap" format="dimension"/>
<!-- 圆角半径,stadium 表示半径为 min(height,width) / 2-->
<attr name="btnCornerRadius" format="dimension">
<enum name="stadium" value="-1"/>
</attr>
<attr name="btnPressedRatio" format="float"/>
</declare-styleable>
设置边框
创建item_background_focus.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="@dimen/cibn_view_stroke_width"
android:color="@color/item_stroke_focus_color"/>
<corners
android:bottomLeftRadius="@dimen/radius_default"//圆角与RoundLinearLayout中的角度一致
android:bottomRightRadius="@dimen/radius_default"
android:topLeftRadius="@dimen/radius_default"
android:topRightRadius="@dimen/radius_default"/>
<padding
android:bottom="@dimen/cibn_view_stroke_padding"
android:left="@dimen/cibn_view_stroke_padding"
android:right="@dimen/cibn_view_stroke_padding"
android:top="@dimen/cibn_view_stroke_padding"/>
</shape>
创建item_background_normal.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="@dimen/cibn_view_stroke_width"
android:color="@color/item_stroke_color"/>
<corners
android:bottomLeftRadius="@dimen/radius_default"
android:bottomRightRadius="@dimen/radius_default"
android:topLeftRadius="@dimen/radius_default"
android:topRightRadius="@dimen/radius_default"/>
<padding
android:bottom="@dimen/cibn_view_stroke_padding"
android:left="@dimen/cibn_view_stroke_padding"
android:right="@dimen/cibn_view_stroke_padding"
android:top="@dimen/cibn_view_stroke_padding"/>
</shape>
创建item_sleclected.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/item_background_focus" android:state_focused="true"/>
<item android:drawable="@drawable/item_background_normal" android:state_focused="false"/>
</selector>
设置item的背景为item_sleclected.xml即可
将自定义的LinearLayout 继承RoundLinearLayout ,即为最终控件。将布局文件中的LinearLayout替换成我们的自定义LinearLayout即为最终效果。(前提是焦点问题要处理好,焦点处理需要注意的点后面会单独介绍)