前言:移动智能设备的发展,推动了安卓另一个领域,包括智能电视和智能家居,以及可穿戴设备的大量使用,但是这些设备上的开发并不是和传统手机开发一样,特别是焦点控制和用户操作体验上有很大的区别,本系列博文主要用TV播放器的实现去了解下在智能设备上的开发的相关技术。点击查看原文

转载请说明出处:http://blog.csdn.net/sk719887916

通过前两篇的学习,(安卓Tv开发(二)焦点控制(键盘事件)) 大家基本了解了安卓事件机制原理,终于间隔三个月后有时间继续完善此系列文章了,下面就开始今天的正题,

本文章将会带大去会实现电视盒子的UI设计,并实现遥控器控制九宫格,并进行翻页效果。

效果:

安卓TV开发(三) 移动智能设备之实现主流TV电视盒子焦点可控UI-LMLPHP

通过分析此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


04-16 06:32
查看更多