安卓TV开发(三) 移动智能设备之实现主流TV电视盒子焦点可控UI
前言:移动智能设备的发展,推动了安卓另一个领域,包括智能电视和智能家居,以及可穿戴设备的大量使用,但是这些设备上的开发并不是和传统手机开发一样,特别是焦点控制和用户操作体验上有很大的区别,本系列博文主要用TV播放器的实现去了解下在智能设备上的开发的相关技术。点击查看原文
转载请说明出处:http://blog.csdn.net/sk719887916
通过前两篇的学习,(安卓Tv开发(二)焦点控制(键盘事件)) 大家基本了解了安卓事件机制原理,终于间隔三个月后有时间继续完善此系列文章了,下面就开始今天的正题,
本文章将会带大去会实现电视盒子的UI设计,并实现遥控器控制九宫格,并进行翻页效果。
效果:
通过分析此UI,我们可以自定义一个类似grideview的自定义控件,再自定义itemvVew,通过键盘方向键即遥控器反向键控制itemview焦点切换。
一:自定义焦点控制的父布局
1 首先自定义一个可控制焦点的Focusview,
新建一个focusview,继承viewgroup,重写构造方法。充当我们的外部框架,类似流式布局,
public FocusView(Context context) {
this(context, null);
}
public FocusView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FocusView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs);
initViewGroup(context);
}
private void initViewGroup(Context context) {
mScroller = new Scroller(context);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
写完以上代码有同学可能对scroller的用法,这里就先略微说一下此类 ,Android里Scroller类是为了实现View平滑滚动的一个Helper类。通常在自定义的View时使用,在View中定义一个私有成员mScroller = new Scroller(context)。设置mScroller滚动的位置时,并不会导致View的滚动,通常是用mScroller记录/计算View滚动的位置,再重写View的computeScroll(),完成实际的滚动。
api具体解释如下
mScroller.getCurrX() //获取mScroller当前水平滚动的位置 mScroller.getCurrY() //获取mScroller当前竖直滚动的位置 mScroller.getFinalX() //获取mScroller最终停止的水平位置 mScroller.getFinalY() //获取mScroller最终停止的竖直位置 mScroller.setFinalX(int newX) //设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置 mScroller.setFinalY(int newY) //设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置 //滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量, duration为完成滚动的时间 mScroller.startScroll(int startX, int startY, int dx, int dy) //使用默认完成时间250ms mScroller.startScroll(int startX, int startY, int dx, int dy, int duration) mScroller.computeScrollOffset() //返回值为boolean,true说明滚动尚未完成,false说明滚动已经完成。这是一个很重要的方法,通常放在View.computeScroll()中,用来判断是否滚动是否结束。
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop()
此处代码为了之后表示滑动的时候,手的移动要大于这个返回的距离值才开始移动控件。
2, 接着 我们重写 onLayout()和onMeasure()方法,今天我们不重点讲解自定义view绘制。
执行测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int height = MeasureSpec.getSize(heightMeasureSpec);
mRowHeight = (height - (visibleRows - 1) * mGapHeight - getPaddingTop() - getPaddingBottom()) / visibleRows;
mColWidth = (width - (visibleCols - 1) * mGapWidth - getPaddingLeft() - getPaddingRight()) / visibleCols;
final int itemCount = mFocusItems.size();
for (int i = 0; i < itemCount; i++) {
final FocusItem item = mFocusItems.get(i);
final View childView = item.getMetroView();
final int childWidth = MeasureSpec.makeMeasureSpec((mColWidth + mGapWidth) * item.getColSpan() - mGapWidth, MeasureSpec.EXACTLY);
final int childHeight = MeasureSpec.makeMeasureSpec((mRowHeight + mGapHeight) * item.getRowSpan() - mGapHeight, MeasureSpec.EXACTLY);
childView.measure(childWidth, childHeight);
}
scrollTo((mColWidth + mGapWidth) * mCurCol, (mRowHeight + mGapHeight) * mCurRow);
}
具体布局:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int itemCount = mFocusItems.size();
if (itemCount != getChildCount())
throw new IllegalArgumentException("contain unrecorded child");
for (int i = 0; i < itemCount; i++) {
final FocusItem item = mFocusItems.get(i);
final View childView = item.getMetroView();
if (childView.getVisibility() != View.GONE) {
final int childLeft = getPaddingLeft() + (mColWidth + mGapWidth) * item.getCol();
final int childTop = getPaddingTop() + (mRowHeight + mGapHeight) * item.getRow();
final int childWidth = (mColWidth + mGapWidth) * item.getColSpan() - mGapWidth;
final int childHeight = (mRowHeight + mGapHeight) * item.getRowSpan() - mGapHeight;
childView.layout(childLeft, childTop, childLeft + childWidth,
childTop + childHeight);
}
}
}
scrollTo()方法将当前获得焦点的view动画移到指定坐标位置。
转载请说明出处:http://blog.csdn.net/sk719887916
等绘制完view,重头戏剧来了,下面我我们就即将重写onTouchEvent()和onInterceptTouchEvent()。
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
final int action = event.getAction();
float x = event.getX();
float y = event.getY();
if (mOrientation == OrientationType.Horizontal)
y = 0;
else if (mOrientation == OrientationType.Vertical)
x = 0;
switch (action) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastMotionX = x;
mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
int deltaX = (int) (mLastMotionX - x);
int deltaY = (int) (mLastMotionY - y);
mLastMotionX = x;
mLastMotionY = y;
scrollBy(deltaX, deltaY);
break;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000);
int velocityX = (int) velocityTracker.getXVelocity();
int velocityY = (int) velocityTracker.getYVelocity();
int row = mCurRow;
int col = mCurCol;
if (velocityX > SNAP_VELOCITY && mCurCol > 0) {
col--;
} else if (velocityX < -SNAP_VELOCITY && mCurCol < mColsCount - 1) {
col++;
}
if (velocityY > SNAP_VELOCITY && mCurRow > 0) {
row--;
} else if (velocityY < -SNAP_VELOCITY && mCurRow < mRowsCount - 1) {
row++;
}
if (row == mCurRow && col == mCurCol)
snapToDestination();
else {
snapTo(row, col);
if (metroListener != null)
metroListener.scrollto(row, col);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mTouchState = TOUCH_STATE_REST;
break;
case MotionEvent.ACTION_CANCEL:
mTouchState = TOUCH_STATE_REST;
break;
}
return true;
}
ps: VelocityTracker主要用跟踪触摸屏事件的速率,getXVelocity() 或getXVelocity()获得横向和竖向的速率到速率时,使用它们之前必须先调用computeCurrentVelocity(int)来初始化速率的单位 。
mVelocityTracker.addMovement(event);
此方法主要是将Motion event加入到VelocityTracker类实例中.用于控制envent,
mVelocityTracker = VelocityTracker.obtain();
方法来获得VelocityTracker类的一个实例需来跟踪触摸屏事件的速度。
onInterceptTouchEvent()是用于处理事件(重点onInterceptTouchEvent这个事件是从父控件开始往子控件传的,直到有拦截或者到没有这个事件的view,然后就往回从子到父控件,这次是onTouch的)(类似于预处理,当然也可以不处理)并改变事件的传递方向,也就是决定是否允许Touch事件继续向下(子控件)传递,一但返回True(代表事件在当前的viewGroup中会被处理),则向下传递之路被截断(所有子控件将没有机会参与Touch事件),同时把事件传递给当前的控件的onTouchEvent()处理;返回false,则把事件交给子控件的onInterceptTouchEvent(),具体请详见两篇文章对事件的描述。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE)
&& (mTouchState != TOUCH_STATE_REST)) {
return true;
}
final float x = ev.getX();
switch (action) {
case MotionEvent.ACTION_MOVE:
final int xDiff = (int) Math.abs(mLastMotionX - x);
if (xDiff > mTouchSlop) {
mTouchState = TOUCH_STATE_SCROLLING;
}
break;
case MotionEvent.ACTION_DOWN:
mLastMotionX = x;
mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
: TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mTouchState = TOUCH_STATE_REST;
break;
}
return mTouchState != TOUCH_STATE_REST;
}
3 重写以上事件方法,目的就是拦截键盘点击事件,将动画设置获得焦点的view上,这样遥控器按建,焦点就随之移动,我们的动画也随之移动。
接下来继续重写onKeyDown()和onKeyUp()
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
FocusItem focusItem = getFocusMetroItem();
if(focusItem == null) {
return false;
}
Log.d(TAG, "in onKeyUp focus.row="+focusItem.getRow()+" focus.col="+focusItem.getCol());
int leftCol = mCurCol;
int topRow = mCurRow;
int rightCol = mCurCol + visibleCols;
int buttomRow = mCurRow + visibleRows;
Log.d(TAG, "in onKeyUp leftCol="+leftCol+" topRow="+topRow+" rightCol="+rightCol+" buttomRow="+buttomRow);
//change page
switch(keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
if(mOrientation != OrientationType.Vertical) {
if(focusItem.getCol() < leftCol) {
scowTo(mCurRow, focusItem.getCol() + focusItem.getColSpan() - visibleCols);
}
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if(mOrientation != OrientationType.Vertical) {
if(focusItem.getCol() + focusItem.getColSpan() > rightCol) {
scowTo(mCurRow, focusItem.getCol());
}
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
if(mOrientation != OrientationType.Horizontal) {
if(focusItem.getRow() < topRow) {
scowTo(focusItem.getRow() + focusItem.getRowSpan() - visibleRows, mCurCol);
}
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if(mOrientation != OrientationType.Horizontal) {
if(focusItem.getRow() + focusItem.getRowSpan() > buttomRow) {
scowTo(focusItem.getRow(), mCurCol);
}
}
break;
}
return super.onKeyUp(keyCode, event);
}
重写以上事件方法,目的就在键盘弹出的时候,焦点随遥控器上下左后方向键随之移动,动画效果也随之移动。
在上面的重写方法我们可以看到了有scowTo()方法;用Ta我们来控制动画是否需要移动,view是否需要重绘制
public void scowTo(int whichRow, int whichCol) {
if (whichRow < 0)
whichRow = 0;
if (whichCol < 0)
whichCol = 0;
Log.d(TAG, String.format("snap to row:%d, col:%d", whichRow, whichCol));
boolean needRedraw = false;
if (mOrientation == OrientationType.Horizontal) {
whichRow = 0;
if (whichCol + visibleCols > mColsCount)
whichCol = Math.max(mColsCount - visibleCols, 0);
} else if (mOrientation == OrientationType.Vertical) {
whichCol = 0;
if (whichRow + visibleRows > mRowsCount)
whichRow = Math.max(mRowsCount - visibleRows, 0);
} else if (mOrientation == OrientationType.All) {
if (whichRow + visibleRows > mRowsCount)
whichRow = Math.max(mRowsCount - visibleRows, 0);
if (whichCol + visibleCols > mColsCount)
whichCol = Math.max(mColsCount - visibleCols, 0);
}
int deltaX = whichCol * (mColWidth + mGapWidth);
int deltaY = whichRow * (mRowHeight + mGapHeight);
Log.e(TAG, "end whichRow="+whichRow+" whichCol="+whichCol +" getScrollX()="+getScrollX()+" getScrollY()="+getScrollY());
if (getScrollX() != deltaX) {
deltaX = deltaX - getScrollX();
needRedraw = true;
}else {
deltaX = 0;
}
if (getScrollY() != deltaY) {
deltaY = deltaY - getScrollY();
needRedraw = true;
}else {
deltaY = 0;
}
if (needRedraw) {
mScroller.startScroll(getScrollX(), getScrollY(), deltaX, deltaY,
Math.max(Math.abs(deltaX)/2, Math.abs(deltaY/2)) * 2);
mCurRow = whichRow;
mCurCol = whichCol;
invalidate();
}
}
重写了以上方法,我们的view算是能用了,主要通过此自定义view,通过遥控器按键控制itemview是否获得焦点,是否可以滚动,
接下来 我们在加入动画效果,新建一各动画类AnimationFocusManager
二 新建AnimationFocusManager
此类控制翻页以及焦点放大动画效果,
/**
* 焦点控制动画控制器.
* @author lyk
*
*/
public class AnimationMetroManager implements OnFocusChangeListener{
int animationIn = -1;
int animationOut = -1;
boolean animationFocusLock = false;
View focusView = null;
HashMap<View, OnFocusChangeListener> focusPool = new HashMap<View, View.OnFocusChangeListener>();
private Context mContext;
public AnimationFocusManager(Context c) {
if(c == null) {
throw new IllegalArgumentException("the context is null");
}
mContext = c;
}
/**
* it is Animation is locked ;
* set AnimationFocusLocked ,
* To facilitate the other animation not to perform
* @param lock
*/
public void setAnimationFocusLock(boolean lock) {
boolean oldLock = animationFocusLock;
if(oldLock == lock) {
return;
}
animationFocusLock = lock;
if(animationFocusLock && animationIn != -1 && focusView != null) {
focusView.startAnimation(AnimationUtils.loadAnimation(mContext, animationIn));
}else if(!animationFocusLock && animationOut != -1 && focusView != null) {
focusView.bringToFront();
focusView.startAnimation(AnimationUtils.loadAnimation(mContext, animationOut));
}
}
public void setAnimation(int in, int out) {
this.animationIn = in;
this.animationOut = out;
}
public boolean isAvailability() {
return !animationFocusLock && animationOut != -1 && animationIn != -1;
}
public void add(View v, OnFocusChangeListener l) {
focusPool.put(v, l);
}
public void delete(View v) {
focusPool.remove(v);
}
public void clear() {
focusPool.clear();
focusView = null;
}
public void onFocusChange(View v, boolean hasFocus) {
if(hasFocus) {
focusView = v;
}
if(hasFocus && isAvailability()) {
Animation anim = AnimationUtils.loadAnimation(mContext, animationOut);
v.bringToFront();
v.startAnimation(anim);
}else if(isAvailability()){
Animation anim = AnimationUtils.loadAnimation(mContext, animationIn);
v.startAnimation(anim);
}
if(focusPool.containsKey(v)) {
focusPool.get(v).onFocusChange(v, hasFocus);
}
}
此类主要是自定义几个方法,设置动画 移,除动画,焦点改变监听等,便于控制焦点,用于view回调。具体不做细说
最后我们不要忘了在自定义的Focusview内初始化此动画
private void initViewGroup(Context context) {
mScroller = new Scroller(context);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
// 加入动画
mAnimationFocusController = new AnimationMetroManager(getContext());
}
此动画用到的动画xml,移动动画我们可以自己设置自定义动画,不限制demo所示动画。
背景动画
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:fillBefore="false"
android:shareInterpolator="false" >
<scale
android:duration="100"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:pivotX="50.0%"
android:pivotY="50.0%"
android:repeatCount="0"
android:toXScale="1.2"
android:toYScale="1.2" />
</set>
获得焦点放大后的动画
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:fillBefore="false"
android:shareInterpolator="false" >
<scale
android:duration="100"
android:fromXScale="1.2"
android:fromYScale="1.2"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:pivotX="50.0%"
android:pivotY="50.0%"
android:repeatCount="0"
android:toXScale="1.0"
android:toYScale="1.0" />
</set>
到这一步我们的view已经算是成功了,我们通过自定义view实现了一个类似流式布局的FocusVew,可以通过键盘移动,并能在获得焦点的view的子view的焦点显示动画效果,但里面我们还需要填充itemview(子控件),下面我们将会继续自定义itemView和怎么使用Focusview,具体代码逻辑下篇 安卓TV开发(四)
实现主流智能TV视频播放器UI。 点击查看原文 会继续讲解,
源码地址:https://github.com/NeglectedByBoss/FocusVIew
安卓TV开发(三) 移动智能设备之实现主流TV电视盒子焦点可控UI的更多相关文章
- Android TV开发总结(六)构建一个TV app的直播节目实例
请尊重分享成果,转载请注明出处:http://blog.csdn.net/hejjunlin/article/details/52966319 近年来,Android TV的迅速发展,传统的有线电视受 ...
- Android TV开发总结(七)构建一个TV app中的剧集列表控件
原文:Android TV开发总结(七)构建一个TV app中的剧集列表控件 版权声明:我已委托"维权骑士"(rightknights.com)为我的文章进行维权行动.转载务必转载 ...
- 安卓TV开发(五) 移动智能终端UI之实现主流TV焦点可控UI
载请标明出处:http://blog.csdn.net/sk719887916,作者:skay 由于其他网站收录,导致你无法查看本系列原创文章请点击此处 安卓TV开发(四)实现主流智能T ...
- delphi 10 seattle 安卓服务开发(三)
delphi 10 里面的安卓服务有四种,上面的一篇文章里面的图有介绍. 今天做一个remote service 的例子.(里面一部分代码是抄别人的,如果不太清楚,自行恶补) remote servi ...
- Android TV开发总结(二)构建一个TV Metro界面(仿泰捷视频TV版)
前言:上篇是介绍构建TV app前要知道的一些事儿,开发Android TV和手机本质上没有太大的区别,屏大,焦点处理,按键处理,是有别于有手机和Pad的实质区别.今天来介绍TV中Metro UI风格 ...
- Android TV开发总结(一)构建一个TV app前要知道的事儿
转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/52792562 前言:近年来,智能 ...
- Android TV开发总结(三)构建一个TV app的焦点控制及遇到的坑
转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/52835829 前言:上篇中,&l ...
- 安卓TV开发(七) 移动智能终端多媒体之在线解析网页视频源
载请标明出处:http://blog.csdn.net/sk719887916/article/details/40049137,作者:skay 结束了所有UI绘制的学习,智能设备常用的应用音视频类, ...
- Win7的64位系统如何搭建安卓Android开发环境
在搭建安卓Android开发环境,那么现在比较主流的Win7的64位操作系统如何搭建呢?其实很简单,不需要设置任何环境变量,只需要下载两个程序包(ADT和JDK),下载的时候注意选择相应的64位程序包 ...
随机推荐
- Android开发过程中在sh,py,mk文件中添加log信息的方法
Android开发过程中在sh,py,mk文件中添加log信息的方法 在sh文件中: echo "this is a log info" + $info 在py文件中: print ...
- How to kill a particular user terminal on Linux
Intro. Sometimes, the application we launched from command promp failed to exit. What we require is ...
- Android源码浅析(五)——关于定制系统,如何给你的Android应用系统签名
Android源码浅析(五)--关于定制系统,如何给你的Android应用系统签名 今天来点简单的我相信很多定制系统的同学都会有一些特定功能的需求,比如 修改系统时间 静默安装 执行某shell命令 ...
- EBS业务学习之库存管理
库存管理业务流程 企业结构 库存结构 库存结构定义 指定每个子库存的特性: •子库存的数量跟踪 •资产类子库存 •保留子库存 •净值子库存 •包含在有效承诺中Include in ATP •子库存级库 ...
- activiti uuid主键
1.1.1. activiti默认主键生成方式 ; 下面我们看一下主键的生成策略:主键的生成策略定义在IdGenerator接口中,接口定义如下所示: public interface IdGene ...
- SpriteKit中的共享动作(Sharing Actions)
大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 在SpriteKit中某些动作需要一些额外的延时,如果每次都重 ...
- 浅谈hibernate+入门实例
Hibernate是对jdbc进一步的封装,随着项目的开展,小编开始接触到这个概念,一开始接触的时候并没有觉得hibernate有多神秘,没有进一步的研究,只是简单的知道她是对jdbc的进一步的封装, ...
- Java进阶(四十一)多线程讲解
Java多线程讲解 前言 接到菜鸟网络的电话面试,面试官让自己谈一下自己对多线程的理解,现将其内容整理如下. 线程生命周期 Java线程具有五种基本状态 新建状态(New):当线程对象创建后,即进入了 ...
- javascript之页面打印
WebBrowser组件是IE内置的浏览器控件,使用时,首先要在<body>标签的下面用<object>...</object>标记声明WebBrowser组件,代 ...
- UNIX网络编程——原始套接字的魔力【下】
可以接收链路层MAC帧的原始套接字 前面我们介绍过了通过原始套接字socket(AF_INET, SOCK_RAW, protocol)我们可以直接实现自行构造整个IP报文,然后对其收发.提醒一点,在 ...