项目感悟:TV端项目的0—1
来源:互联网 发布:诉苦大会 知乎 编辑:程序博客网 时间:2024/05/21 22:49
0—1之外:
对于TV端的Android项目,其实并没有想象中的那么神奇,与普通的Android应用一样,全部都是Android自己的东西,只是在TV端,还需要考虑的问题之一就是焦点移动问题(我觉得这是制约TV端app发展的一个最主要的问题),怎样在使用上下左右,确定,返回,菜单键来控制整个应用的操作。假如,我们能像操作手机那样操作电视,而且在流畅程度上与手机并无差异的话,那该是多么美好的一件事情。试想一下,如果有个可以模拟电视界面的东西来让用户点击,这个场面将会很美。说了这么多其他的东西,我们来说说项目的问题。一个项目从无到有其实需要经历很多东西,但是作为一个开发人员来说,我们需要关心的最为主要的就是怎么更好的开发这款产品。
0 过程:
在开发这个项目的时候,我们首先要确定,做什么,怎么做。当然,对于开发人员来说,做什么已经不要我们考虑了,我们要完成的事情就是考虑怎么做。由于以前有做过和这个项目类似的东西,所以在整体的框架上面,我们就延用了上一个项目的东西,一来,省去了很多事情,二来,规避掉了很多不必要的风险。
总结一下我们在项目中用到的主要的东西:(要我自己整理他们的用法,可能还有一定的难度,可能附上大神们的网址来的更快,更好一些)
1、EventBus:点击打开链接
2、Gson:点击打开链接
3、Fragment:点击打开链接
4、ScaleRelativeLayout:是一个TV端经常用到的东西-选中放大,比起不同的选中操作,来的更加有灵动性;
5、RoundedImageView:用于对于图片切圆角很有用,比起Android自己切圆角方法要圆润的多;
6、Glide:一个优质的图片加载工具
7、RecycleView:点击打开链接
8、MediaPlayer+SurfaceView:用于对视频的播放。
以上几个东西就是我们在项目中主要用的到的一些东西,使用现成的东西固然方便,但在使用之后,要对它进行分析和理解,毕竟只有真正理解之后才会有更好的使用结果产生。确定完怎么来做这个东西之后,下面剩下的问题就是合作开发了,这个就是从0到1的一个过程,这个过程就不用再仔细描述了把,相信每一个程序员们对这个过程都再熟悉不过了。
1 过程:
当项目成型之后,在这个过程中需要弄很多的东西,包括项目中出现的bug问题,项目中实现方式不合理的地方,项目中考虑问题不全面的地方,都会这这里体现出来。虽然说这里面有的问题应该在项目开发过程中,或者是项目开始开发之前就应该想得到,但是这是在拥有丰富经验的前提条件之下才可能的,并且这个并不是完全可以规避的问题,如果能全部都考虑完全了,那么所有的项目都只有一次的开发周期。在这个过程中,或多或少会遇到改需求的问题,对于这个问题,我们要辩证的看待。对于我们来说,本来实现完成就是一件很付出心血的事情,但是突然不要,心理或多或少会有一些想法,但是考虑到能有更好的用户体验,这个问题还是可以放在一边的。同时在这个过程中,可以更好的优化项目中的一些东西,比如项目中的一些流畅性问题,比如一些更合理的用户操作方式,再比如一些酷炫的效果实现,都可以在加入到这个项目中去。我感觉在这个阶段的好坏,会关系到这个产品的好坏。
0—1 之间:
总结完成之后,我们就来开讨论一下在项目中遇到的一些问题,和分享一些项目中用的一些方法。
1、Glide固然是一种好用的图片加载工具,它的简单用法,和他的一些效果都是那么的让人喜欢,在项目中,我们就遇到了一个这样的问题,在每次首次加载一组图片时,都会出现加载不出来,只显示默认图的情况,当再次切回来的时候,图片又能很好地显示出来,导致这个问题的主要原因是我们使用了重载过的ImageView,就会引起这样的问题,当然我们后面使用的解决办法是这样。
<pre name="code" class="java"><span style="font-size:14px;">SimpleTarget simpleTarget = new SimpleTarget<Bitmap>() {</span>@Override public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) { imageView.setImageBitmap(resource); } };Glide.with(context.getApplicationContext()) .load(imageUrl) .asBitmap() .fitCenter() .placeholder(R.drawable.default_bg_big) .error(R.drawable.default_bg_big)
.priority(Priority.HIGH) .into(simpleTarget);这样就能很好地解决这个问题,当然也牺牲掉了一些别的东西,比如说加载时的动效。起初还考虑将每一张默认图都设置成gif的形式,但是考虑到性能的问题就放弃了。
2、RecycleView获取第一个item的焦点,或者是获取其中某一个位置的焦点:
对于刚刚初始化好的一个RecycleView来说要让它的第一个item获取焦点其实很简单就使用这样的一个方式就可以:
<span style="font-size:14px;">recyclerViewList.getChildAt(0).requestFocus();</span>
但是这样的方式会产生一个NullPointerException,造成的原因是在初始化好了的时候,RecycleView里面还有没有完全准备好来获得他的第一个item焦点,这样就需要使用Handler来做一个延时操作来解决这个问题,虽然说这个解决方法有点牵强,但是也是一个好办法,不是吗。在获取RecycleView中的其中某一个item的焦点时,会有这样的问题,RecycleView的初始化方式是只显示屏幕中能看到的item,每次item在屏幕中的位置是固定不变的,比如说我们要回去100item中的第50个焦点的话,就会有一定的困难,后来我们找到了这个问题的解决办法:
handler.postDelayed(new Runnable() { @Override public void run() { linearLayoutManager.scrollToPositionWithOffset(currentPosition, 0); } }, 200); handler.postDelayed(new Runnable() { @Override public void run() { RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(currentPosition); ((VideoPlayerAdapter.MyHolder) viewHolder).layout.requestFocus(); } }, 300);这样就能完美的解决上面说的那个问题。
3、在使用MeidaPlayer的时候,除了遵循它的生命周期,还要注意的一个事情就是,在Player reset或者是release的时候,这两个是耗时操作,如果它们将他们两个放在主线程里面来处理的话,可能会照成页面卡顿,最有效的方法是(对就是你想的那样)将他们放在线程里面去处理。
4、焦点问题一直是TV端的一个重要问题,在项目中,我们遇到了焦点乱跳的问题,这个问题造成的原因有几个,可能是数据重新加载,导致界面发生变化,焦点就会移动到最近的位置,或者是你想不到的地方;也有可能是一边的焦点已经移动到最后了,由于就近原则,它会移动到别的地方去,等等一些原因会造成焦点乱跳的问题,所以在处理这些问题时,要合理的采用不同的方法来解决,屏蔽掉它的案件事件,比如说在某种状态下,希望他不响应下键,或者是其他件,就在onKeyDown 那个里面返回一个true就搞定了,或者是使用的setNextFucus...()方法来设置,或者是其他的一些办法来解决,只要用心想,总会找到解决的办法的。
5、由于放大的原因,会造成itemVIew被遮挡的问题,这个问题其实很好解,比如说由于padding造成的原因,我们可以在它的父类view中设置这两个属性就解决了:
android:clipChildren="false" android:clipToPadding="false"或者是使用view里面的一个方法来手动设置一个变量来把它调整到显示的最顶端:
@Override protected int getChildDrawingOrder(int childCount, int i) { // TODO Auto-generated method stub if(pos<0){ return i; } else{ if(i==childCount -1){ if(pos>i) pos=i; return pos; } if(i==pos){ return childCount -1; } } return i; }
6、在使用滚动的时候,如果想让一个滚动的效果更加圆润的话可以使用这样的方法:
ValueAnimator animator = ValueAnimator.ofFloat(1, 0f); animator.setDuration(400); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float f = (Float) animation.getAnimatedValue(); horizontalScrollView.smoothScrollTo((int) (x*f),0); } }); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { isScrollEnd = false; isScrollStart = true; } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animator.start();
7、然后在分享一个RecycleView居中的类,它能保证我们在RecycleView中焦点移动时,除了边界上的几个其他的都是保持在中间的位置上:
public class AppRecyclerView extends RecyclerView {private int mTopPosition = -1;public static final int CHANGE_TYPE_PAGE = 0;public static final int CHANGE_TYPE_CENTER = 1;public static final int CHANGE_TYPE_NONE = 3;private int mChangeType = CHANGE_TYPE_NONE;private static final int DEFAULT_EDGE_OFFSET = 60;private int mEdgeOffset = DEFAULT_EDGE_OFFSET;public AppRecyclerView(Context arg0) {super(arg0);setChildrenDrawingOrderEnabled(true);}public AppRecyclerView(Context arg0, AttributeSet arg1) {super(arg0, arg1);setChildrenDrawingOrderEnabled(true);TypedArray a= getContext().obtainStyledAttributes(arg1, R.styleable.appRecyclerView);mChangeType=a.getInt(R.styleable.appRecyclerView_changeType,CHANGE_TYPE_NONE);Log.i("mChangeType",mChangeType+"");}public AppRecyclerView(Context arg0, AttributeSet arg1, int arg2) {super(arg0, arg1, arg2);setChildrenDrawingOrderEnabled(true);}/** * 设置切换效果,默认为切页。<br/> * 切页:CHANGE_TYPE_PAGE<br/> * 居中:CHANGE_TYPE_CENTER<br/> * 原生:CHANGE_TYPE_NONE * * @param tType */public void setChangeType(int tType) {mChangeType = tType;}@Overrideprotected int getChildDrawingOrder(int childCount, int i) {if (i == 0) {mTopPosition = -1; // 重绘时使标记位失效}View tView = getChildAt(i);if (tView != null) {if (tView.isFocused()) { // 焦点位置最后绘制,保持在顶层mTopPosition = i;return childCount - 1;} else {if (i == childCount - 1 && mTopPosition != -1) { // 最后一个位置与焦点位置交替return mTopPosition;}}}return i;}@Overridepublic boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {switch (mChangeType) {case CHANGE_TYPE_PAGE:return requestVerticalPageChange(child, rect);case CHANGE_TYPE_CENTER:return requestHorizontalCenterChange(child, rect);case CHANGE_TYPE_NONE:return requestVerticalCenterChange(child, rect);default:return super.requestChildRectangleOnScreen(child, rect, immediate);}}/** * 设置边缘的差分滚动距离,解决在边缘处按键无法调到下一个位置导致的焦点不可控问题;<br/> * 建议设置略大于item之间的间隔值, 默认值为60px<br/> * @param tOffset */public void setEdgeOffset(int tOffset){mEdgeOffset = tOffset;}@Overridepublic boolean dispatchKeyEvent(KeyEvent event) {if (event.getAction() == KeyEvent.ACTION_DOWN) {//if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN){//return true;//}View tView = getFocusedChild();if (tView != null) {switch (event.getKeyCode()) {case KeyEvent.KEYCODE_DPAD_UP:if (FocusFinder.getInstance().findNextFocus(this, tView, View.FOCUS_UP) == null) {int tPosition = getChildAdapterPosition(tView);if (tPosition != 0) {scrollBy(0,-mEdgeOffset);if (event.getRepeatCount() > 1){return true;}}}break;case KeyEvent.KEYCODE_DPAD_DOWN:if (FocusFinder.getInstance().findNextFocus(this, tView, View.FOCUS_DOWN) == null) {int tPosition = getChildAdapterPosition(tView);if (tPosition != getAdapter().getItemCount() - 1) {scrollBy(0,mEdgeOffset);if (event.getRepeatCount() > 1){return true;}}}break;default:break;}}}return super.dispatchKeyEvent(event);}/** * 水平切页效果 * * @param child * @param rect * @return */private boolean requestHorizontalPageChange(View child, Rect rect) {final int parentLeft = getPaddingLeft();final int parentRight = getWidth() - getPaddingRight();final int childLeft = child.getLeft() + rect.left - child.getScrollX();final int childRight = childLeft + rect.width();final int offScreenLeft = Math.min(0, childLeft - parentLeft);final int offScreenRight = Math.max(0, childRight - parentRight);// Favor the "start" layout direction over the end when bringing one// side or the other// of a large rect into view. If we decide to bring in end because start// is already// visible, limit the scroll such that start won't go out of bounds.int dx;if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {// 右到左的局部dx = offScreenRight != 0 ? offScreenRight : Math.max(offScreenLeft, childRight - parentRight);} else {dx = offScreenLeft != 0 ? offScreenLeft : Math.min(childLeft - parentLeft, offScreenRight);}if (dx != 0) {if (dx > 0) {dx = dx + getWidth() - child.getWidth() - child.getWidth() / 2;} else {dx = dx - getWidth() + child.getWidth() + child.getWidth() / 2;}this.smoothScrollBy(dx, 0);return true;}return false;}private boolean requestVerticalPageChange(View child, Rect rect) {final int parentTop = getPaddingTop();final int parentBottom = getHeight() - getPaddingBottom();final int childTop = child.getTop() + rect.top - child.getScrollY();final int childBottom = childTop + rect.height();final int offScreenTop = Math.min(0, childTop - parentTop);final int offScreenBottom = Math.max(0, childBottom - parentBottom);// Favor the "start" layout direction over the end when bringing one// side or the other// of a large rect into view. If we decide to bring in end because start// is already// visible, limit the scroll such that start won't go out of bounds.int dy;if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {// 右到左的局部dy = offScreenBottom != 0 ? offScreenBottom : Math.max(offScreenTop, childBottom - parentBottom);} else {dy = offScreenTop != 0 ? offScreenTop : Math.min(childTop - parentTop, offScreenBottom);}if (dy != 0) {if (dy > 0) {dy = dy + getHeight() - child.getHeight() - child.getHeight() / 2;} else {dy = dy - getHeight() + child.getHeight() + child.getHeight() / 2;}this.smoothScrollBy(0, dy);return true;}return false;}/** * 竖直居中效果 * * @param child * @param rect * @return */private boolean requestVerticalCenterChange(View child, Rect rect) {final int parentTop = getPaddingTop();final int parentBottom = getHeight() - getPaddingBottom();final int childTop = child.getTop() + rect.top - child.getScrollY();final int childBottom = childTop + rect.height();int dy = 0;final int offScreenTop = childTop - (parentTop + getHeight() / 2 - child.getHeight() / 2);final int offScreenBottom = childBottom - (parentBottom + getHeight() / 2 - child.getHeight() / 2);if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { // 右到左的局部dy = offScreenBottom;} else {dy = offScreenTop;}if (dy != 0) {this.smoothScrollBy(0, dy);return true;}return false;}/** * 水平居中效果 * * @param child * @param rect * @return */private boolean requestHorizontalCenterChange(View child, Rect rect) {final int parentLeft = getPaddingLeft();final int parentRight = getWidth() - getPaddingRight();final int childLeft = child.getLeft() + rect.left - child.getScrollX();final int childRight = childLeft + rect.width();int dx = 0;final int offScreenLeft = childLeft - (parentLeft + getWidth() / 2 - child.getWidth() / 2);final int offScreenRight = childRight - (parentRight + getWidth() / 2 - child.getWidth() / 2);if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { // 右到左的局部dx = offScreenRight;} else {dx = offScreenLeft;}if (dx != 0) {this.smoothScrollBy(dx, 0);return true;}return false;}}item选中放大的ScaleRelativeLayout :
public class ScaleRelativeLayout extends RelativeLayout implements SpringListener {private Spring mSpring;private float SPRING_MIN_VALUE = 1;private float SPRING_MAX_VALUE = 1.1f;public ScaleRelativeLayout(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}public void setSPRING_MAX_VALUE(float SPRING_MAX_VALUE) {this.SPRING_MAX_VALUE = SPRING_MAX_VALUE;}public ScaleRelativeLayout(Context context, AttributeSet attrs) {super(context, attrs);}public ScaleRelativeLayout(Context context) {super(context);}private void render() {float val = (float) mSpring.getCurrentValue();this.setScaleX(val);this.setScaleY(val);}public void scaleOut() {if (null == mSpring) {mSpring = SpringSystem.create().createSpring().addListener(this).setSpringConfig(SpringConfig.fromOrigamiTensionAndFriction(100, 6));mSpring.setCurrentValue(SPRING_MIN_VALUE);}mSpring.setEndValue(SPRING_MAX_VALUE);}public void scaleIn() {if (null == mSpring) {mSpring = SpringSystem.create().createSpring().addListener(this).setSpringConfig(SpringConfig.fromOrigamiTensionAndFriction(100, 6));}mSpring.setEndValue(SPRING_MIN_VALUE);}@Overridepublic void onSpringActivate(Spring arg0) {this.setLayerType(View.LAYER_TYPE_HARDWARE, null);}@Overridepublic void onSpringAtRest(Spring arg0) {this.setLayerType(View.LAYER_TYPE_NONE, null);}@Overridepublic void onSpringEndStateChange(Spring arg0) {}@Overridepublic void onSpringUpdate(Spring arg0) {render();}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();}@Overrideprotected void onAttachedToWindow() {super.onAttachedToWindow();}}
最后,再给自己提一点小目标——写出干净整洁的代码。
- 项目感悟:TV端项目的0—1
- 项目感悟(1)
- 项目感悟
- 项目感悟
- 项目感悟
- 项目感悟
- 项目感悟
- 第一次做项目的感悟
- tv助手项目阶段性总结
- 基于 android TV的智能家居项目的传感器浅谈(1)
- 东半球最好的TV桌面开源项目
- Android Tv 焦点移动特效项目学习经验1
- 项目快速开发的几点感悟
- 项目中关于clone的一些感悟
- 一个项目组长的感悟(一)
- 一个项目组长的感悟(二)
- 第一个ssh项目的感悟
- 做web 项目的几点感悟
- mysql定时器执行存储过程
- 雅虎34条军规
- Building Web Apps with Go
- 【Arduino】【ESP】使用Arduino(ESP8266版本)获取雅虎天气信息,并使用点阵显示......进行中
- HDU 4405 Aeroplane chess (概率DP)——2012 ACM/ICPC Asia Regional Jinhua Online
- 项目感悟:TV端项目的0—1
- JavaScript 运算符
- 课堂笔记
- ubuntu时区设置
- 数据库知识复习
- Android程序崩溃统一处理机制
- Maven软件管理学习
- nmon集群资源使用情况 监控工具
- swift闭包的使用 -- 类似于OC中的Block