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

本篇文章已授权微信公众号DriodDeveloper(逆流的鱼yuiop)独家发布

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

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

  • 1,在所有需要放大和设置边框的View下方嵌套一层FrameLayout,作为放大的背景的容器。焦点移动上去,算出当前View的大小,然后再设置FrameLayout的大小与.9图片并bringtoFront();
  • 2,为每个需要放大与突出的View设置shape和selector,这个是我最推荐的方法,现在很多TV的APP都采用这种,但是有个缺点,发光和阴影并不能设置。这与需要稍微有点炫酷效果的桌面有点不符合。
  • 3,全局FrameLayout,这个是我现在在用的方法,现在已经整理成一套框架,不久就会开源,现在还有示例Demo未完成。

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

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

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

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

/**
 * Created by ShanCanCan on 2016/4/3 0003.
 */

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 主要实现边框的生成与移动,还有阴影的添加

/**
 *
 * Created by ShanCanCan on 2016/4/3 0003.
 */

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;
        setWillNotDraw(false);//必须要设置,如果我们想要重写onDraw,就要调用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。



    @Override
    public void drawMoveView(Canvas canvas) {
        canvas.save();

        if (!isDrawUpRect) {//飞框的绘制顺序,

            onDrawShadow(canvas);

            onDrawUpRect(canvas);
        }
        // 绘制焦点子控件.
        if (mFocusView != null && (!isDrawUpRect && isDrawing)) {
            onDrawFocusView(canvas);
        }
        //
        if (isDrawUpRect) {//飞框的绘制顺序

            onDrawShadow(canvas);

            onDrawUpRect(canvas);
        }
        canvas.restore();

    }
  @Override
    public void setFocusView(View currentView, View oldView, float scale) {

        mFocusView = currentView;
        int getScale = (int) (scale * 10);
        if (getScale > 10) {
            if (currentView != null) {

                currentView.animate().scaleX(scale).scaleY(scale).setDuration(DEFAULT_TRAN_DUR_ANIM).start();
                if (oldView != null) {
                    oldView.animate().scaleX(DEFUALT_SCALE).scaleY(DEFUALT_SCALE).setDuration(DEFAULT_TRAN_DUR_ANIM).start();
                }
            }
        }
        rectMoveAnimation(currentView, scale, scale);

    }

    @Override
    public void rectMoveAnimation(View currentView, float scaleX, float scaleY) {
        Rect fromRect = findLocationWithView(getMoveView());
        Rect toRect = findLocationWithView(currentView);
        int disX = toRect.left - fromRect.left;
        int disY = toRect.top - fromRect.top;
        rectMoveMainLogic(currentView, disX, disY, scaleX, scaleY);
    }

    private Rect findLocationWithView(View view) {
        ViewGroup root = (ViewGroup) getMoveView().getParent();
        Rect rect = new Rect();
        root.offsetDescendantRectToMyCoords(view, rect);
        return rect;
    }

    private void rectMoveMainLogic(final View focusView, float x, float y, float scaleX, float scaleY) {
        int newWidth = 0;
        int newHeight = 0;
        int oldWidth = 0;
        int oldHeight = 0;
        if (focusView != null) {
            newWidth = (int) (focusView.getMeasuredWidth() * scaleX);
            newHeight = (int) (focusView.getMeasuredHeight() * scaleY);
            x = x + (focusView.getMeasuredWidth() - newWidth) / 2;
            y = y + (focusView.getMeasuredHeight() - newHeight) / 2;
        }

        // 取消之前的动画.
        if (mCombineAnimatorSet != null)
            mCombineAnimatorSet.cancel();

        oldWidth = getMoveView().getMeasuredWidth();
        oldHeight = getMoveView().getMeasuredHeight();

        ObjectAnimator transAnimatorX = ObjectAnimator.ofFloat(getMoveView(), "translationX", x);
        ObjectAnimator transAnimatorY = ObjectAnimator.ofFloat(getMoveView(), "translationY", y);
        ObjectAnimator scaleXAnimator = ObjectAnimator.ofInt(new ScaleTool(getMoveView()), "width", oldWidth,
                (int) newWidth);
        ObjectAnimator scaleYAnimator = ObjectAnimator.ofInt(new ScaleTool(getMoveView()), "height", oldHeight,
                (int) newHeight);
        //
        AnimatorSet mAnimatorSet = new AnimatorSet();
        mAnimatorSet.playTogether(transAnimatorX, transAnimatorY, scaleXAnimator, scaleYAnimator);
        mAnimatorSet.setInterpolator(new DecelerateInterpolator(1));
        mAnimatorSet.setDuration(DEFAULT_TRAN_DUR_ANIM);
        getMoveView().setVisibility(View.VISIBLE);

        mAnimatorSet.addListener(new Animator.AnimatorListener() {//动画的监听,主要用来设置移动飞框的绘制顺序
            @Override
            public void onAnimationStart(Animator animation) {
                if (!isDrawUpRect)
                    isDrawing = false;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                if (!isDrawUpRect)
                    isDrawing = false;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (!isDrawUpRect)
                    isDrawing = true;
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                if (!isDrawUpRect)
                    isDrawing = false;}
        });

        mAnimatorSet.start();
        mCombineAnimatorSet = mAnimatorSet;

    }

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



    /**
     * 绘制最上层的移动边框.
     */
    public void onDrawUpRect(Canvas canvas) {
        Drawable drawableUp = getMoveView().getUpRectDrawable();
        if (drawableUp != null) {
            RectF paddingRect = getMoveView().getDrawUpRect();//从MoveView()中获取的,你可以自己在activity调节。
            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) {
            RectF shadowPaddingRect = getMoveView().getDrawShadowRect();//从MoveView()中获取的,你可以自己在activity调节。
            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 ,用来包裹我们所有的控件,这样,主要是为了放大的时候,控件不会被挡住

package com.shancancan.tvdemos.views;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.RelativeLayout;

/**
 *
 * Created by ShanCanCan on 2016/4/3 0003.
 */

public class MainRelativeLayout extends RelativeLayout {

    private int position;



    public MainRelativeLayout(Context context) {
        super(context);
        init(context);
    }

    public MainRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public MainRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }



        private void init(Context context){
            setClipChildren(false); //是否现限制其他控件在它周围绘制选择false
            setClipToPadding(false); //是否限制控件区域在padding里面
            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"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:background="@drawable/default_wallpaper"
    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() {
        mMoveView.setUpRectResource(R.drawable.conner);//这里也可以设置shape或者是.9图片
        float density = getResources().getDisplayMetrics().density;//调整大小,如果你的边框大了就修改w_或者h_这两个参数
        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.setUpPaddingRect(receF);//重新为mMoveView设置大小
        mMoveView.setTranDurAnimTime(400);
    }
    public float getDimension(int id) {
        return getResources().getDimension(id);
    }
    private void initRelativeLayout() {//这是焦点的全局监听方法,与OnFocusChangeListener不同,这个方法长安不执行。

        mRelativeLayout.getViewTreeObserver().addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
            @Override
            public void onGlobalFocusChanged(View oldFocus, View newFocus) {

                if (newFocus != null) {
                   // newFocus.bringToFront();
                    mMoveView.setDrawUpRectEnabled(true);//设置居于放大的view之上。
                    float scale = 1.1f;
                    mMoveView.setFocusView(newFocus, mOldFocus, scale);
                    mMoveView.bringToFront();//将mMoveView的位置bringToFront()
                    mOldFocus = newFocus;//自己将移动后的View进行保存,

                }
            }
        });
    }
}

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

Demo尚未完成,先传百度云,点击即可下载与CSDN下载,完成后我会将其上传至Jcenter和github,大家直接compile就行了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容