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

    xiaoxiao2021-04-12  31

    本篇文章已授权微信公众号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尚未完成,先传百度云,点击即可下载与下载,完成后我会将其上传至Jcenter和github,大家直接compile就行了。

    转载请注明原文地址: https://ju.6miu.com/read-668301.html

    最新回复(0)