美文网首页Android开发
android tv 焦点移动控件-飞框-FlowView

android tv 焦点移动控件-飞框-FlowView

作者: ihu11 | 来源:发表于2019-10-16 15:38 被阅读0次

github库---demo源码-https://github.com/ihu11/MetroRecyclerView

1.先来一张效果图展示焦点移动控制

device-2019-10-16-145843.gif device-2019-10-16-145301.gif

功能概述
1、焦点框独立于整个界面之上,可以与直接与其他控件和view的交互,且自己管理自己的绘制线程,所以焦点框可以在屏幕的任意位置上移动,不局限于当前的列表控件。也方便了统一的管理
2、主要实现了焦点框的平滑移动,只需要知道下一个焦点的view就可以根据这个view的位置把焦点框移动过去。
3、在移动过程中,如果选择了其他焦点,该焦点框会根据下一个焦点的位置自动转移方向,不需要等下上一次的焦点事件完成,可以时刻响应焦点移动事件的改变。
4、不管下一个焦点与当前焦点的大小是否一样,焦点框都是准确到框住下一个焦点的区域,焦点框的大小根据焦点位置和区域的大小自动改变。
5、焦点框的资源图片也可以动态改变,适应不同需求的焦点框,例如从圆角的方框移动到一个直角的方框上,通过切换焦点框的资源图而改变形状。
6、开放可重载方法,提供其他类继承,可在该控件上再定制其他效果,例如在焦点框上增加动画效果
7、焦点框直接绘制不需要地图,可以变换为任意颜色和形状

流程图

代码实现

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.TextView;

import com.ihu11.metro.flow.FlowNormalView;
import com.ihu11.metro.flow.FlowView;

public class MainActivity extends Activity implements View.OnFocusChangeListener, View.OnClickListener {
    private FlowView flowView;
    private TextView textView1;
    private TextView textView2;
    private TextView textView3;
    private TextView textView4;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        flowView = findViewById(R.id.flow_view);
        textView1 = findViewById(R.id.text1);
        textView2 = findViewById(R.id.text2);
        textView3 = findViewById(R.id.text3);
        textView4 = findViewById(R.id.text4);
        textView1.setOnFocusChangeListener(this);
        textView2.setOnFocusChangeListener(this);
        textView3.setOnFocusChangeListener(this);
        textView4.setOnFocusChangeListener(this);
        textView1.setOnClickListener(this);
        textView2.setOnClickListener(this);
        textView3.setOnClickListener(this);
        textView4.setOnClickListener(this);

        textView4.setNextFocusDownId(R.id.text1);
        textView1.setNextFocusUpId(R.id.text4);

        requestViewFocus(textView1);
    }

    @Override
    public void onFocusChange(View view, boolean b) {
        if (b) {
            if (view == textView1) {
                //flowView.setSmooth(false);//直接到 不使用动画
            } else if (view == textView2) {
                flowView.setFlowPadding(20, 20, 20, 20);//增加边框距离
            } else if (view == textView3) {
                flowView.setNextShape(FlowNormalView.SHAPE_RECT);//变成直角方形
            } else if (view == textView4) {
                flowView.setNextShape(FlowNormalView.SHAPE_ROUND);//变成圆边方形
            }
            flowView.moveTo(view, 1.0f);
        }
    }

    public static void requestViewFocus(final View view) {
        if (view.getWidth() > 0 && view.getHeight() > 0) {
            view.requestFocus();
            return;
        }
        view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

            @SuppressWarnings("deprecation")
            @Override
            public void onGlobalLayout() {
                int width = view.getWidth();
                int height = view.getHeight();
                if (width > 0 && height > 0) {
                    view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                    view.setFocusable(true);
                    view.requestFocus();
                }
            }
        });
    }

    @Override
    public void onClick(View view) {
        if (view == textView3) {
            startActivity(new Intent(this, GridActivity.class));
        } else if (view == textView2) {
            startActivity(new Intent(this, HorActivity.class));
        } else if (view == textView1) {
            startActivity(new Intent(this, VerActivity.class));
        } else if (view == textView4) {
            startActivity(new Intent(this, ModifyActivity.class));
        }
    }
}

布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffB0C4DE">

    <TextView
        android:id="@+id/text1"
        android:layout_width="100px"
        android:layout_height="50px"
        android:layout_marginLeft="200px"
        android:layout_marginTop="200px"
        android:focusable="true"
        android:gravity="center"
        android:text="垂直列表"
        android:textColor="#ffffffff" />

    <TextView
        android:id="@+id/text2"
        android:layout_width="110px"
        android:layout_height="50px"
        android:layout_marginLeft="200px"
        android:layout_marginTop="300px"
        android:focusable="true"
        android:gravity="center"
        android:text="水平列表"
        android:textColor="#ffffffff" />

    <TextView
        android:id="@+id/text3"
        android:layout_width="120px"
        android:layout_height="50px"
        android:layout_marginLeft="200px"
        android:layout_marginTop="400px"
        android:focusable="true"
        android:gravity="center"
        android:text="网格列表"
        android:textColor="#ffffffff" />

    <TextView
        android:id="@+id/text4"
        android:layout_width="160px"
        android:layout_height="50px"
        android:layout_marginLeft="200px"
        android:layout_marginTop="500px"
        android:focusable="true"
        android:gravity="center"
        android:text="可删除项的列表"
        android:textColor="#ffffffff" />

    <com.ihu11.metro.flow.FlowView
        android:id="@+id/flow_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:flow_color1="@android:color/white"
        app:flow_color2="@android:color/black"
        app:viewType="normal"
        app:flow_stroke_width="2px"
        app:round_radius="5px" />

</RelativeLayout>

FlowView

package com.ihu11.metro.flow;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

import com.ihu11.metro.R;

public class FlowView extends ViewGroup {

    public final static int VIEW_TYPE_NORMAL = 0;
    public final static int VIEW_TYPE_SOLID = 1;
    public final static int VIEW_TYPE_NO_SHADOW = 2;

    protected FlowNormalView iFlowView;
    private int viewType;
    private float roundRadius;

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

    public FlowView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlowView(Context context) {
        this(context, null);
    }

    private void init(Context context, AttributeSet attrs) {
        int color1 = -1;
        int color2 = -1;
        int strokeWidth = -1;
        if (attrs != null) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlowView);
            viewType = a.getInt(R.styleable.FlowView_viewType, VIEW_TYPE_NORMAL);
            roundRadius = a.getDimensionPixelSize(R.styleable.FlowView_round_radius, 0);
            color1 = a.getResourceId(R.styleable.FlowView_flow_color1, -1);
            color2 = a.getResourceId(R.styleable.FlowView_flow_color2, -1);
            strokeWidth = a.getResourceId(R.styleable.FlowView_flow_stroke_width, -1);
            a.recycle();
        } else {
            viewType = VIEW_TYPE_NORMAL;
        }

        if (viewType == VIEW_TYPE_NORMAL) {
            iFlowView = new FlowNormalView(context);
            addView(iFlowView);
        } else if (viewType == VIEW_TYPE_SOLID) {
            iFlowView = new FlowSolidView(context);
            addView(iFlowView);
        } else if (viewType == VIEW_TYPE_NO_SHADOW) {
            iFlowView = new FlowNormalView(context);
            iFlowView.setNoShawdow();
            addView(iFlowView);
        }

        iFlowView.setDefaultRadius(roundRadius);
        iFlowView.setStrokeWidth(strokeWidth);
        int c1 = -1;
        if (color1 != -1) {
            c1 = getResources().getColor(color1);
        }
        int c2 = -1;
        if (color2 != -1) {
            c2 = getResources().getColor(color2);
        }
        iFlowView.setRectColor(c1, c2);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int cCount = getChildCount();
        for (int i = 0; i < cCount; i++) {
            View childView = getChildAt(i);
            childView.layout(l, t, r, b);
        }
    }

    @Override
    public void setVisibility(int visibility) {
        super.setVisibility(visibility);
        int cCount = getChildCount();
        for (int i = 0; i < cCount; i++) {
            View childView = getChildAt(i);
            childView.setVisibility(visibility);
        }
    }

    public void moveTo(View view, float scale) {
        if (iFlowView != null) {
            iFlowView.moveTo(view, scale);
        }
    }

    public void setFlowPadding(int left, int top, int right, int bottom) {
        if (iFlowView != null) {
            iFlowView.setFlowPadding(left, top, right, bottom);
        }
    }

    public void moveTo(float x, float y, float width, float height, float scale) {
        if (iFlowView != null) {
            iFlowView.moveTo(x, y, width, height, scale);
        }
    }

    public void moveTo(View view, float scale, int offsetX, int offsetY, boolean isSmooth) {
        if (iFlowView != null) {
            iFlowView.moveTo(view, scale, offsetX, offsetY, isSmooth);
        }
    }

    public void setOffset(int x, int y) {
        if (iFlowView != null) {
            iFlowView.setOffset(x, y);
        }
    }

    public void setSmooth(boolean isSmooth) {
        if (iFlowView != null) {
            iFlowView.setSmooth(isSmooth);
        }
    }

    public void setNextShape(int shape) {
        if (iFlowView != null) {
            iFlowView.setNextShape(shape);
        }
    }

    public void setRectColor(int rectColor, int shadowColor) {
        if (iFlowView != null) {
            iFlowView.setRectColor(rectColor, shadowColor);
        }
    }

    public void setStrokeWidth(float strokeWidth) {
        iFlowView.setStrokeWidth(strokeWidth);
    }

    public void setDefaultRadius(float roundRadius) {
        iFlowView.setDefaultRadius(roundRadius);
    }
}

FlowNormalView

package com.ihu11.metro.flow;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;

public class FlowNormalView extends View {

    public final static int SHAPE_ROUND_RECT = 0;
    public final static int SHAPE_RECT = 1;
    public final static int SHAPE_ROUND = 2;

    private final static String TAG = "FlowHand";
    private final static float SPEED = 2f;// 默认速度
    private final static long MIN_FLOW_TIME = 200;// 最小时间
    private final static long MAX_FLOW_TIME = 300;// 最大时间
    private final static float SHADOW_TOTAL_WIDTH = 40f;
    private final static float SHADOW_STROKE_WIDTH = 1f;
    private final static int SHADOW_ALPHA_START = 150;
    private static final Interpolator mInterpolator1 = new AccelerateDecelerateInterpolator();
    private static final Interpolator mInterpolator2 = new DecelerateInterpolator();
    private static final Interpolator mInterpolatorShadow = new AccelerateInterpolator();
    private static final int INTERVAL_TIME_NORMAL = 20;

    private boolean isRun = false;
    private int offsetX = 0;
    private int offsetY = 0;
    private int paddingLeft = 0;
    private int paddingRight = 0;
    private int paddingTop = 0;
    private int paddingBottom = 0;
    private int shape = SHAPE_ROUND_RECT;
    private float strokeWidth;
    protected float screenScale;
    private boolean isSmooth = true;
    private boolean isContinuityMove = false;
    private boolean noShawdow = false;

    private int rectColor = 0xFFFFFFFF;
    private int shadowColor = 0xFF000000;

    private RoundRectF lastRectF;
    private RoundRectF currRectF;
    private RoundRectF destRectF;
    private RoundRectF shadowRectF;

    protected Paint mPaint;
    private Paint mPaintShadow;
    private Paint paintEraser;
    private float defaultRadius;

    private long startTime = 0L;
    private long totalTime = 0L;
    private float distance = 0f;

    class RoundRectF extends RectF {
        float radius;

        public RoundRectF() {
        }

        public void set(RoundRectF src) {
            this.left = src.left;
            this.right = src.right;
            this.top = src.top;
            this.bottom = src.bottom;
            this.radius = src.radius;
        }

    }

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

    public FlowNormalView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public FlowNormalView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public FlowNormalView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    protected void init(Context context) {

        lastRectF = new RoundRectF();
        currRectF = new RoundRectF();
        destRectF = new RoundRectF();
        shadowRectF = new RoundRectF();

        screenScale = context.getResources().getDisplayMetrics().widthPixels / 1920f;
        strokeWidth = 2f * screenScale;

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(rectColor);
        mPaint.setStrokeWidth(strokeWidth);
        mPaint.setStyle(Paint.Style.STROKE);

        mPaintShadow = new Paint();
        mPaintShadow.setAntiAlias(true);
        mPaintShadow.setColor(shadowColor);
        mPaintShadow.setStyle(Paint.Style.STROKE);

        paintEraser = new Paint();
        paintEraser.setAntiAlias(true);
        paintEraser.setColor(Color.BLACK);
        paintEraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
    }

    private void start() {
        if (isRun) {
            return;
        }
        isRun = true;
        new Thread() {
            public void run() {
                while (isRun) {
                    refreshView();
                }
                isRun = false;
            }
        }.start();
    }

    protected void refreshView() {
        try {
            Thread.sleep(INTERVAL_TIME_NORMAL);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        postInvalidate();
    }

    protected void moveToLocation(float l, float t, float width, float height, float roundScale) {
        resetLast();

        // 偏移量
        l -= offsetX;
        t -= offsetY;

        destRectF.left = l - paddingLeft;
        destRectF.right = l + width + paddingRight;
        destRectF.top = t - paddingTop;
        destRectF.bottom = t + height + paddingBottom;
        if (shape == SHAPE_ROUND_RECT) {
            destRectF.radius = defaultRadius * roundScale;
        } else if (shape == SHAPE_RECT) {
            destRectF.radius = 0f;
        } else if (shape == SHAPE_ROUND) {
            destRectF.radius = Math.min(destRectF.bottom - destRectF.top, destRectF.right - destRectF.left) / 2;
        }

        startTime = System.currentTimeMillis();
        distance = getDistance(lastRectF.centerX(), lastRectF.centerY(), destRectF.centerX(), destRectF.centerY());

        if (isSmooth) {
            if (lastRectF.width() < 0.1f || lastRectF.height() < 0.1f) {
                totalTime = 0;
            } else {
                totalTime = (long) (distance / SPEED);
                if (totalTime > MAX_FLOW_TIME) {
                    totalTime = MAX_FLOW_TIME;
                } else if (totalTime < MIN_FLOW_TIME) {
                    totalTime = MIN_FLOW_TIME;
                }
            }
        } else {
            totalTime = 0;
        }
        reset();
        start();
    }

    private float getDistance(float x1, float y1, float x2, float y2) {
        return (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    }

    private float getCurrDistance() {
        long time = System.currentTimeMillis();
        if (time >= startTime + totalTime) {
            return distance;
        }
        float p = (time - startTime) / (float) totalTime;
        if (isContinuityMove) {
            return mInterpolator2.getInterpolation(p) * distance;
        } else {
            return mInterpolator1.getInterpolation(p) * distance;
        }
    }

    private void resetLast() {
        if (Math.abs(getCurrDistance() - distance) < 0.1f) {
            isContinuityMove = false;
        } else {
            isContinuityMove = true;
        }

        lastRectF.set(currRectF);
    }

    @Override
    public void onDraw(Canvas canvas) {
        drawView(canvas);
    }

    private void drawView(Canvas canvas) {
        float currDistance = getCurrDistance();
        if (distance != 0) {
            currRectF.left = lastRectF.left + (destRectF.left - lastRectF.left) * (currDistance / distance);
            currRectF.right = lastRectF.right + (destRectF.right - lastRectF.right) * (currDistance / distance);
            currRectF.top = lastRectF.top + (destRectF.top - lastRectF.top) * (currDistance / distance);
            currRectF.bottom = lastRectF.bottom + (destRectF.bottom - lastRectF.bottom) * (currDistance / distance);
            currRectF.radius = lastRectF.radius + (destRectF.radius - lastRectF.radius) * (currDistance / distance);
        }

        if (currRectF.width() > 0.1f && currRectF.height() > 0.1f) {
            drawRoundRectF(canvas, currRectF);
            drawExtra(canvas, currRectF);
        }

        if (Math.abs(currDistance - distance) < 0.1f) {
            isContinuityMove = false;
            isRun = false;
        }
    }

    protected void drawExtra(Canvas canvas, RectF currRectF) {

    }

    protected void drawRoundRectF(Canvas canvas, RoundRectF currRectF) {
        shadowRectF.set(currRectF);
        if (!noShawdow) {
            mPaintShadow.setStrokeWidth(SHADOW_STROKE_WIDTH);
            for (float i = 0f; i < SHADOW_TOTAL_WIDTH; i += SHADOW_STROKE_WIDTH) {
                shadowRectF.left = currRectF.left - i;
                shadowRectF.top = currRectF.top - i;
                shadowRectF.right = currRectF.right + i;
                shadowRectF.bottom = currRectF.bottom + i;
                shadowRectF.radius = currRectF.radius + i;
                float p = mInterpolatorShadow.getInterpolation(((SHADOW_TOTAL_WIDTH - i) / SHADOW_TOTAL_WIDTH));
                mPaintShadow.setAlpha((int) (p * SHADOW_ALPHA_START));
                canvas.drawRoundRect(shadowRectF, shadowRectF.radius, shadowRectF.radius, mPaintShadow);
            }
        }
        canvas.drawRoundRect(currRectF, currRectF.radius, currRectF.radius, mPaint);
    }

    public void setNoShawdow() {
        noShawdow = true;
    }

    public void setDefaultRadius(float defaultRadius) {
        this.defaultRadius = defaultRadius;
    }

    public void setNextShape(int shape) {
        this.shape = shape;
    }

    /**
     * 设置偏移
     *
     * @param x X轴的偏移量
     * @param y Y轴的偏移量
     */
    public void setOffset(int x, int y) {
        offsetX = x;
        offsetY = y;
    }

    public void setSmooth(boolean isSmooth) {
        this.isSmooth &= isSmooth;
    }

    private void reset() {
        offsetX = 0;
        offsetY = 0;
        isSmooth = true;
        paddingLeft = 0;
        paddingRight = 0;
        paddingTop = 0;
        paddingBottom = 0;
        shape = SHAPE_ROUND_RECT;
    }

    /**
     * 移动到view的位置
     *
     * @param view
     * @param scale    是否缩放
     * @param offsetX  X轴的偏移量
     * @param offsetY  Y轴的偏移量
     * @param isSmooth 是否平滑滚动
     */
    public void moveTo(View view, float scale, int offsetX, int offsetY, boolean isSmooth) {
        if (view == null) {
            Log.e(TAG, "view is null");
            return;
        }
        setOffset(offsetX, offsetY);
        setSmooth(isSmooth);
        moveTo(view, scale);
    }

    public void moveTo(View view, float scale) {
        int h = view.getHeight();
        int w = view.getWidth();
        // Log.d(TAG, "width:" + w);
        int[] location = new int[2];
        view.getLocationInWindow(location);
        int x = location[0];
        int y = location[1];

        x += ((view.getScaleX() - 1) / 2) * w;
        y += ((view.getScaleY() - 1) / 2) * h;

        moveTo(x, y, w, h, scale);
    }

    public void moveTo(float x, float y, float width, float height, float scale) {
        if (width > 0 && height > 0) {
            int[] location = new int[2];
            ((ViewGroup) getParent().getParent()).getLocationInWindow(location);
            int pX = location[0];
            int pY = location[1];
            float l = x - pX;
            float t = y - pY;
            moveToLocation(l - ((scale - 1) / 2) * width, t - ((scale - 1) / 2) * height, width * scale, height * scale,
                    scale);
        } else {
            reset();
            Log.e(TAG, "width or height is 0");
        }
    }

    public void setFlowPadding(int left, int top, int right, int bottom) {
        this.paddingLeft = left;
        this.paddingRight = right;
        this.paddingTop = top;
        this.paddingBottom = bottom;
    }

    public void setRectColor(int rectColor, int shadowColor) {
        if (rectColor != -1) {
            this.rectColor = rectColor;
            mPaint.setColor(rectColor);
        }
        if (shadowColor != -1) {
            this.shadowColor = shadowColor;
            mPaintShadow.setColor(shadowColor);
        }
    }

    public void setStrokeWidth(float strokeWidth) {
        if (strokeWidth > 0) {
            this.strokeWidth = strokeWidth;
            mPaint.setStrokeWidth(strokeWidth);
        }
    }
}

FlowSolidView

package com.ihu11.metro.flow;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.support.annotation.Nullable;
import android.util.AttributeSet;

public class FlowSolidView extends FlowNormalView {

    private LinearGradient shader;

    private int color1;
    private int color2;

    public FlowSolidView(Context context) {
        super(context);
    }

    public FlowSolidView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public FlowSolidView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public FlowSolidView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    protected void init(Context context) {
        super.init(context);
        mPaint.setStyle(Paint.Style.FILL);
    }

    @Override
    public void setRectColor(int color1, int color2) {
        this.color1 = color1;
        this.color2 = color2;
    }

    @Override
    protected void drawRoundRectF(Canvas canvas, RoundRectF rectF) {
        if (color1 != color2) {
            shader = new LinearGradient(rectF.left,
                    rectF.top,
                    rectF.right,
                    rectF.top,
                    color1,
                    color2,
                    Shader.TileMode.CLAMP);
            mPaint.setShader(shader);
        } else {
            mPaint.setColor(color1);
        }

        canvas.drawRoundRect(rectF, rectF.radius, rectF.radius, mPaint);
    }
}

相关文章

网友评论

    本文标题:android tv 焦点移动控件-飞框-FlowView

    本文链接:https://www.haomeiwen.com/subject/vxlumctx.html