两步集成 TV 移动框架,从未如此简单

4,006 阅读11分钟

导读 TV相关的资料网上相对来说少点,我早期写了七篇TV相关的开发总结,有开源了一些Demo,在我的github上,今天是单灿灿同学独家在本公众平台发布他最新开源的TV框架。单灿灿的blog地址是:http://blog.csdn.net/zhcswlp0625/。



从初TV开发到现在,在移动边框上用过很多方法。

下面我来简单的列出来使用过那些解决方法和思路:

  • 1,在所有需要放大和设置边框的View下方嵌套一层FrameLayout,作为放大的背景的容器。焦点移动上去,算出当前View的大小,然后再设置FrameLayout的大小与.9图片并bringtoFront();

  • 2,为每个需要放大与突出的View设置shape和selector,这个是我最推荐的方法,现在很多TV的APP都采用这种,但是有个缺点,发光和阴影并不能设置。这与需要稍微有点炫酷效果的桌面有点不符合。

  • 3,全局FrameLayout,这个是我现在在用的方法,现在已经整理成一套框架,不久就会开源,现在还有示例Demo未完成。

下面让我们来进入我的框架的主题来看一下:

红圈所标出来的是几个主要的类与自定义View,下面我们来深入(我在设计的时候,焦点处理是各自处理各自的,解耦)。

先上两幅比较难的界面(重点在于焦点的处理与动画的处理,图一有动态的添加和删除)。

最主要的接口MoveAnimationHelper(做动画效果的)如下:

public interface MoveAnimationHelper {
        void drawMoveView(Canvas canvas);//绘制MoveView
        //放大缩小函数
        void setFocusView(View currentView, View oldView, float scale);  
        // 边框移动函数
        void rectMoveAnimation(View currentView, float scaleX, float scaleY);
         MoveFrameLayout getMoveView(); //边框view
        void setMoveView(MoveFrameLayout moveView);//setMoveView
        void setTranDurAnimTime(int time);//设置移动时间
        //是否凸出显示
        void setDrawUpRectEnabled(boolean isDrawUpRect);
     }

MoveFrameLayout是全局的移动飞框,就像文章开头的1的实现类似,但是全局只有一个。

最主要的绘制函数就是 MoveFrameLayout这个类了,这个类就是我们的边框移动 View,这个 View 主要实现边框的生成与移动,还有阴影的添加

public class MoveFrameLayout extends FrameLayout {
    private static final String TAG = "MoveFramLayout";
    private Context mContext;
    private Drawable mRectUpDrawable;
    private Drawable mRectUpShade;
    private MoveAnimationHelper mMoveAnimationHelper;
    private RectF mShadowPaddingRect = new RectF();
    private RectF mUpPaddingRect = new RectF();
    public MoveFrameLayout(Context context) {
        super(context);
        init(context);
    }
    public MoveFrameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }
    public MoveFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }
    private void init(Context context) {
        mContext = context;
 //必须要设置,如果我们想要重写onDraw,就要调用setWillNotDraw(false)
        setWillNotDraw(false);
 //动画的实现类,接下来就要讲解
        mMoveAnimationHelper = new MoveAnimationHelperImplement();
        mMoveAnimationHelper.setMoveView(this);
    }
    /*下面的方法基本是调用MoveAnimationHelperImplement的实现方法,
      来进行我们的放大缩小以及其他展示*/
    public void setFocusView(View currentView, View oldView, float scale) {
        mMoveAnimationHelper.setFocusView(currentView, oldView, scale);
    }
    public View getUpView() {
        return this;
    }
    @Override
    protected void onDraw(Canvas canvas) {
        if (mMoveAnimationHelper != null) {
            mMoveAnimationHelper.drawMoveView(canvas);
            return;
        }
        super.onDraw(canvas);
    }
    public void setUpRectResource(int id) {
        try {
           // 移动的边框.
            this.mRectUpDrawable = mContext.getResources().getDrawable(id); 
            invalidate();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void setUpRectShadeResource(int id) {
        // 移动的边框.
        this.mRectUpShade = mContext.getResources().getDrawable(id); 
        invalidate();
    }
    public Drawable getShadowDrawable() {
        return this.mRectUpShade;
    }
    public Drawable getUpRectDrawable() {
        return this.mRectUpDrawable;
    }
    public RectF getDrawShadowRect() {
        return this.mShadowPaddingRect;
    }
    public RectF getDrawUpRect() {
        return this.mUpPaddingRect;
    }
    public void setUpPaddingRect(RectF upPaddingRect) {
        mUpPaddingRect = upPaddingRect;
    }
    public void setShadowPaddingRect(RectF shadowPaddingRect) {
        mShadowPaddingRect = shadowPaddingRect;
    }
    public  void setTranDurAnimTime(int defaultTranDurAnim) {
        mMoveAnimationHelper.setTranDurAnimTime(defaultTranDurAnim);
    }
    public void setDrawUpRectEnabled(boolean isDrawUpRect) {
        mMoveAnimationHelper.setDrawUpRectEnabled(isDrawUpRect);
    }
 }

MoveAnimationHelperImplement,MoveAnimationHelper的实现者。

这是这个类里面最主要的方法setFocusView。

下面是绘制边框和绘制阴影的方法,这次方法中可以动态的调节移动边框的大小,实现全包裹或者是类似于padding的效果。

  /**
     * 绘制最上层的移动边框.
     */
    public void onDrawUpRect(Canvas canvas) {
        Drawable drawableUp = getMoveView().getUpRectDrawable();
        if (drawableUp != null) {
            //从MoveView()中获取的,你可以自己在activity调节。
            RectF paddingRect = getMoveView().getDrawUpRect();
            int width = getMoveView().getWidth();
            int height = getMoveView().getHeight();
            Rect padding = new Rect();
            // 边框的绘制.
            drawableUp.getPadding(padding);
            drawableUp.setBounds((int) (-padding.left + 
            (paddingRect.left)), 
            (int) (-padding.top + (paddingRect.top)),
                    (int) (width + padding.right - (paddingRect.right)), 
            (int) (height + padding.bottom - (paddingRect.bottom)));
            drawableUp.draw(canvas);
        }
    }
    /**
     * 绘制外部阴影.
     */
    public void onDrawShadow(Canvas canvas) {
        Drawable drawableShadow = getMoveView().getShadowDrawable();
        if (drawableShadow != null) {
            //从MoveView()中获取的,你可以自己在activity调节。
            RectF shadowPaddingRect = getMoveView().getDrawShadowRect();
            int width = getMoveView().getWidth();
            int height = getMoveView().getHeight();
            Rect padding = new Rect();
            drawableShadow.getPadding(padding);
            drawableShadow.setBounds((int) (-padding.left +
            (shadowPaddingRect.left)), (int) (-padding.top +
            (shadowPaddingRect.top)),
                    (int) (width + padding.right - 
            (shadowPaddingRect.right)),
                    (int) (height + padding.bottom - 
            (shadowPaddingRect.bottom)));
            drawableShadow.draw(canvas);
        }
    

根部局所采用的方法是继承RelativeLayout

最上层的layout ,用来包裹我们所有的控件,这样,主要是为了放大的时候,控件不会被挡住

 public class MainRelativeLayout extends RelativeLayout {
    private int position;
    public MainRelativeLayout(Context context) {
        super(context);
        init(context);
    }
 

  •    private void init(Context context){

  •       //是否现限制其他控件在它周围绘制选择false       setClipChildren(false);       //是否限制控件区域在padding里面       setClipToPadding(false);       //用于改变控件的绘制顺序       setChildrenDrawingOrderEnabled(true);       getViewTreeObserver()        .addOnGlobalFocusChangeListener(new ViewTreeObserver.          OnGlobalFocusChangeListener() {           @Override           public void onGlobalFocusChanged(View oldFocus,                View newFocus) {                position = indexOfChild(newFocus);                if (position != -1) {                    bringChildToFront(newFocus);                  // 然后让控件重画,这样会好点                     newFocus.postInvalidate();。                 }                }        });     }    /**     * 此函数 dispatchDraw 中调用.     * 原理就是和最后一个要绘制的view,交换了位置.     * 因为dispatchDraw最后一个绘制的view是在最上层的.     * 这样就避免了使用 bringToFront 导致焦点错乱问题.     */    @Override    protected int getChildDrawingOrder(int childCount, int i) {        if (position != -1) {            if (i == childCount - 1){                return position;            }            if (i == position)                return childCount - 1;        }        return i;    } }

  • 使用方法两步走:

    一,布局文件

    <?xml version="1.0" encoding="utf-8"?>
        <com.shancancan.tvdemos.views.MainRelativeLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            xmlns:app="http://schemas.android.com/apk/res-auto"
            android:id="@+id/activity_entry"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipChildren="false"
            android:clipToPadding="false"
            tools:context="com.shancancan.tvdemos.activities.EntryActivity">
         <!--android:clipChildren="false"//是否现限制其他控件在它周围绘制选择false
            android:clipToPadding="false" //是否限制控件区域在padding里面
            根部局必须要加这两句话,其它父布局按需添加
            布局文件最下方介绍-->
            <com.shancancan.tvdemos.views.RoundImageView
                android:layout_width="200dp"
                android:layout_height="200dp"
                android:scaleType="fitXY"
                android:focusable="true"
                app:borderRadius="5dp"
                app:type="round"
                android:src="@drawable/beijing7"
                android:layout_alignParentBottom="true"
                android:layout_alignParentStart="true"
                android:layout_marginBottom="28dp"
                android:id="@+id/roundImageView3"/>
            <com.shancancan.tvdemos.views.RoundImageView
                android:layout_width="200dp"
                android:layout_height="200dp"
                app:borderRadius="5dp"
                app:type="round"
                android:focusable="true"
                android:scaleType="fitXY"
                android:src="@drawable/beijing5"
                android:layout_alignTop="@+id/roundImageView3"
                android:layout_alignStart="@+id/roundImageView4"
                android:id="@+id/roundImageView5"/>
            <com.shancancan.tvdemos.views.RoundImageView
                android:layout_width="400dp"
                android:layout_height="200dp"
                app:borderRadius="5dp"
                app:type="round"
                android:focusable="true"
                android:scaleType="fitXY"
                android:src="@drawable/beijing3"
                android:id="@+id/roundImageView7"
                android:layout_alignTop="@+id/roundImageView5"
                android:layout_toEndOf="@+id/roundImageView5"
                android:layout_marginStart="117dp"/>
            <com.shancancan.tvdemos.views.RoundImageView
                android:layout_width="200dp"
                android:layout_height="200dp"
                app:borderRadius="15dp"
                app:type="round"
                android:focusable="true"
                android:scaleType="fitXY"
                android:src="@drawable/beijing1"
                android:id="@+id/roundImageView2"/>
            <com.shancancan.tvdemos.views.RoundImageView
                android:layout_width="200dp"
                android:layout_height="200dp"
                app:borderRadius="5dp"
                app:type="round"
                android:focusable="true"
                android:scaleType="fitXY"
                android:src="@drawable/beijing4"
                android:id="@+id/roundImageView6"
                android:layout_alignParentTop="true"
                android:layout_alignParentEnd="true"
                android:layout_marginEnd="128dp"/>
            <com.shancancan.tvdemos.views.RoundImageView
                android:layout_width="300dp"
                android:layout_height="100dp"
                android:scaleType="fitXY"
                android:src="@drawable/beijing6"
                app:borderRadius="5dp"
                app:type="round"
                android:focusable="true"
                android:id="@+id/roundImageView4"
                android:layout_alignBottom="@+id/roundImageView2"
                android:layout_toStartOf="@+id/roundImageView6"
                android:layout_marginEnd="34dp"/>
         <!--MoveFrameLayout必须在根布局之上,而且不能被其他的控件位置上有引用-->
         <com.shancancan.tvdemos.views.MoveFrameLayout
                android:id="@+id/entrymove"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content">
            </com.shancancan.tvdemos.views.MoveFrameLayout>
         <!--根布局用MainRelativeLayout-->
        </com.shancancan.tvdemos.views.MainRelativeLayout>


    二,activity处理

    public class EntryActivity extends AppCompatActivity {
            private static final String TAG = "MainActivity";
            MainRelativeLayout mRelativeLayout;
            MoveFrameLayout mMoveView;
            View mOldFocus;
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_entry);
                mRelativeLayout = (MainRelativeLayout) findViewById(R.id.activity_entry);
                mMoveView = (MoveFrameLayout) findViewById(R.id.entrymove);
                mMoveViewsetDetail();
                initRelativeLayout();
            }
            private void mMoveViewsetDetail() {
                //这里也可以设置shape或者是.9图片
                mMoveView.setUpRectResource(R.drawable.conner);
                //调整大小,如果你的边框大了就修改w_或者h_这两个参数
                float density = getResources().getDisplayMetrics().density;
                RectF receF = new RectF(-getDimension(R.dimen.w_5) 
                 * density, -getDimension(R.dimen.h_5) * density,
                        -getDimension(R.dimen.w_5) * density, 
                -getDimension(R.dimen.h_5) * density);
                //重新为mMoveView设置大小
                mMoveView.setUpPaddingRect(receF);
                mMoveView.setTranDurAnimTime(400);
            }
            public float getDimension(int id) {
                return getResources().getDimension(id);
            }
            //这是焦点的全局监听方法,与OnFocusChangeListener不同,
            //这个方法长安不执行。
            private void initRelativeLayout() {
                mRelativeLayout.getViewTreeObserver().
                addOnGlobalFocusChangeListener(
                new ViewTreeObserver.
                OnGlobalFocusChangeListener() {
                    @Override
                    public void onGlobalFocusChanged(
                    View oldFocus, View newFocus) {
                        if (newFocus != null) {
                           // newFocus.bringToFront();
                            //设置居于放大的view之上。
                            mMoveView.setDrawUpRectEnabled(true);
                            float scale = 1.1f;
                            mMoveView.setFocusView(newFocus, mOldFocus, scale);
                            //将mMoveView的位置bringToFront()
                            mMoveView.bringToFront();
                            //自己将移动后的View进行保存,
                            mOldFocus = newFocus;
                        }
                    }
                });
            }
         }

    大功告成了,简单吧?你可以先下载体验一下,也可以关注我,后续提供更多示例,RecyclerView,带有指示器的ViewPager等等。

    以上对应源码下载地址:http://pan.baidu.com/s/1gfqWwVp(ps:后期完善里面的坑,开源github,敬请关注)


    更多原创TV系列文章,可点击下方文章对应链接:


    Android TV开发总结(一)构建一个TV app前要知道的事儿

    Android TV开发总结(二)构建一个TV Metro界面(仿泰捷视频TV版)

    Android TV开发总结(三)构建一个TV app的焦点控制及遇到的坑

    Android TV开发总结(四)通过RecycleView构建一个TV app列表页(仿腾讯视频TV版)

    Android TV开发总结(五)TV上屏幕适配总结

    Android TV开发总结(六)构建一个TV app的直播节目实例

    Android TV开发总结(七)构建一个TV app中的剧集列表控件


    第一时间获得 不止个人原创 android/音视频技术干货,问题深度总结,FrameWork源码解析,插件化研究,FFmpeg研究,直播技术,最新开源项目推荐,还有更多职场思考 ,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码