github库---demo源码-https://github.com/ihu11/MetroRecyclerView
1.先来一张效果图展示焦点移动控制


功能概述
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);
}
}
网友评论