前言:移动智能设备的发展,推动了安卓另一个领域,包括智能电视和智能家居,以及可穿戴设备的大量使用,但是这些设备上的开发并不是和传统手机开发一样,特别是焦点控制和用户操作体验上有很大的区别,本系列博文主要用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