安卓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位程序包 ...
随机推荐
- Java第10次实验(数据库)
参考资料 数据结构实验参考文件 MySql操作视频与数据库相关jar文件请参考QQ群文件. 第1次实验 1. MySQL数据库基本操作 完整演示一遍登录.打开数据库.建表.插入 常见错误:语句后未跟; ...
- [Matlab+C/C++] 读写二进制文件
introduction 因为Matlab操作简单.方便,它被应用于很多领域:音频处理,图像处理,数值计算等.尽管MATLAB容易操作,但受限于他的语言解释机制,MATLAB的执行速度通常较低.C/C ...
- oh forever love~
npm install -g forever forever start c9sdk/server.js --listen 0.0.0.0 --port 80 -a aa:111 -w ~ To el ...
- nginx 日志分析工具goaccess
参考:https://www.goaccess.io/download 安装 $ wget http://tar.goaccess.io/goaccess-1.1.1.tar.gz $ tar -xz ...
- Eclipse调试(1)——基础篇
作为使用Eclipse的程序员都会使用它的Debug.但是有不少人只会用F6.F8,其他功能知之甚少.今天我就来总结一下我在使用eclipse的debug时的一些个人经验.水平有限,不足之处还请赐教. ...
- Android反编译(未混淆的apk)
Android反编译(未混淆的apk) 工具 dex2jar 下载地址:我的CSDN 或者 官网 jd-gui 下载地址:我的CSDN 或者 官网 反编译步骤 1. 将APK解压缩,获取classes ...
- Weblogic 12c 集群环境搭建
本文是在windows7操作系统下配置的,jdk版本1.7 ,weblogic版本12.1.3.0.0. 搭建集群前的规划 其中AdminServer是总控制端,server1.server2.ser ...
- 【SSH系列】Hibernate映射-- 多对一单向关联映射
在hibernate中非常重要的就是映射,在前面的博文中,小编简单的介绍了基本映射,基本映射是对一个实体进行映射,关联映射就是处理多个实体之间的关系,将关联关系映射到数据库中,所谓的关联关系在对象模型 ...
- J2EE进阶(十九)FileNotFoundException: http://hibernate.org/dtd/hibernate-mapping-3.0.dtd
J2EE进阶(十九)Nested exception: java.io.FileNotFoundException: http://hibernate.org/dtd/hibernate-mappin ...
- Redis 学习笔记1:CentOS 6.7下安装Redis
在linux环境搭建Redis环境,首先从官网(http://redis.io/)下载Redis 版本,本人使用的3.21版本. 1. 将redis 解压到 /usr/local目录下. [root ...