Android发光特效焦点框-遥控器版本

适用于android智能电视的全局焦点框控件,可以省去为每个按钮设计focused的按钮图标。

效果描述:

1.完整适配各种尺寸的图标按钮

2.平滑的补件动画切换焦点

3.点击效果闪光+声音

import android.content.Context;
import android.graphics.drawable.AnimationDrawable;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationSet;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.ImageView;


/**
 * 焦点框控件
 * @author jiangyuchen
 * @date 2014-2-13
 */
public class BorderView extends ImageView implements AnimationListener {
	protected static final String TAG = "BestSetting.BorderView";
	private static int BORDER_SIZE = 20;
	private static int TRAN_DUR_ANIM = 250;
	private SoundPool sp;
	private Context mContext;
	public BorderView(Context context, AttributeSet attrs) {
		super(context, attrs);
		mContext = context;
	}

	/**
	 * 设置边界框的外框大小
	 * @param padding
	 */
	public void setBorderSize(int size){
		BORDER_SIZE = size;
	}
	
	
	/**
	 * 设置位移动画时间
	 * @param dur
	 */
	public void setTranslateAnimtionDuration(int dur){
		TRAN_DUR_ANIM = dur;
	}
	
	public void setLocation(View view){
		ViewLocation location = findLocationWithView(view);
//		Log.v(TAG, "setLocation X:"+location.x+" Y:"+location.y);
		mLeft = location.x-(int)BORDER_SIZE;
		mTop = location.y-(int)BORDER_SIZE;
		mRight = location.x+(int)BORDER_SIZE+view.getWidth();
		mBottom = location.y+(int)BORDER_SIZE+view.getHeight();
		this.layout(mLeft, mTop, mRight, mBottom);
		this.clearAnimation();
		BorderView.this.setVisibility(View.VISIBLE);
	}
	
	private int mLeft, mTop, mRight, mBottom;
	
	@Override
	protected void onLayout(boolean changed, int left, int top, int right,
			int bottom) {
		super.onLayout(changed, left, top, right, bottom);
		if(this.mLeft != left || mTop != top || mRight != right || mBottom != bottom){
			this.layout(this.mLeft, this.mTop, this.mRight, this.mBottom);
		}
	}
	
	/**
	 * 获取View的位置
	 * @param view 获取的控件
	 * @return 位置
	 */
	public ViewLocation findLocationWithView(View view){
		int[] location = new int[2];
		view.getLocationOnScreen(location);
	    return new ViewLocation(location[0], location[1]);
	}
	
	private AnimationDrawable  mBoxBgAnim;
	/**
	 * 初始化焦点框动画
	 */
	public void runBorderAnimation(){
		this.setBackgroundResource(R.anim.box_normal);
		restartBoxAnim();
	}
	
	/**
	 * 重启闪烁动画
	 * @param context
	 */
	public void restartBoxAnim(){
		BorderView.this.setVisibility(View.VISIBLE);
		this.clearAnimation();
		if(mBoxBgAnim == null){
			mBoxBgAnim = (AnimationDrawable) this.getBackground(); 
		}
		if(mBoxBgAnim.isRunning()){
			mBoxBgAnim.stop();
		}
		mBoxBgAnim.start();
		this.startAnimation(AnimUtils.buildAnimBoxNormal(mContext));
	}
	
	/**
	 * 记录上一次的焦点组件,用于判断是否未移动控件的焦点,相同则不重新加载动画
	 */
	private View mLastFocusView;
	/**
	 * 启动焦点框位移动画
	 */
	public void runTranslateAnimation(View toView) {
		runBorderAnimation();
		if(toView == null || mLastFocusView == toView){
			return;
		}
		//缩放比例
		float scaleWValue =  (float) this.getWidth()/((float) toView.getWidth()+2*BORDER_SIZE);
		float scaleHValue =  (float) this.getHeight()/((float) toView.getHeight()+2*BORDER_SIZE);
		ScaleAnimation scale = new ScaleAnimation( scaleWValue, 1.0f,
				scaleHValue,1.0f);
		//记录位置信息,以为启动动画前box已经设置到目标位置了。
		ViewLocation fromLocation = findLocationWithView(this);
		ViewLocation toLocation = findLocationWithView(toView);
		TranslateAnimation tran = new TranslateAnimation(-toLocation.x+(float)BORDER_SIZE+fromLocation.x, 0,
				-toLocation.y+(float)BORDER_SIZE+fromLocation.y, 0);
//		Log.v("TAG","fromX:"+(-toLocation.x+(float)BORDER_SIZE+fromLocation.x)+" fromY:"+(-toLocation.y+(float)BORDER_SIZE+fromLocation.y));
//		Log.v("TAG","fromX:"+fromLocation.x+ " toX:" +toLocation.x+" fromY:"+fromLocation.y+" toY:"+toLocation.x);
//		TranslateAnimation tran = new TranslateAnimation(0, toLocation.x-(float)BORDER_SIZE-fromLocation.x,
//				0, toLocation.y-(float)BORDER_SIZE-fromLocation.y);
		AnimationSet boxAnimaSet = new AnimationSet(true);
		boxAnimaSet.setAnimationListener(this);
		boxAnimaSet.addAnimation(scale);
		boxAnimaSet.addAnimation(tran);
		boxAnimaSet.setDuration(TRAN_DUR_ANIM);
		BorderView.this.setVisibility(View.INVISIBLE);
		setLocation(toView);//先位移到目标位置再启动动画
		Log.v(TAG, "setLocation runTranslateAnimation");
		BorderView.this.startAnimation(boxAnimaSet);
		mLastFocusView = toView;
	}
	
	public void playClickOgg(){
		if(sp == null){
			sp =new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
			sp.load(mContext, R.raw.djtx, 0);
		}
		sp.play(1, 1, 1, 0, 0, 1);
	}
	private  static AnimationSet mBoxAnimClick;
	private void runClickAnimtion(){
		playClickOgg();
		if(mBoxAnimClick == null){
			mBoxAnimClick = AnimUtils.buildAnimBoxClick(mContext);
		}
		BorderView.this.startAnimation(mBoxAnimClick);
		notifyRestartBoxAnim(500);
	}
	
	public static final int MSG_BOX_BG_ANIM = 10;
	public static final int MSG_BOX_CLICK_ANIM = 11;
	
	/**
	 * 重启背景动画
	 * @param delay 延迟时间毫秒
	 */
	void notifyRestartBoxAnim(int delay){
		mBoxHandler.sendEmptyMessageDelayed(MSG_BOX_BG_ANIM, delay);
	}
	/**
	 * 点击动画
	 */
	public void notifyClickBoxAnim(){
		mBoxHandler.sendEmptyMessageDelayed(MSG_BOX_CLICK_ANIM, 10);
	}
	
	Handler mBoxHandler = new Handler(){
		public void handleMessage(android.os.Message msg) {
			switch(msg.what){
			case MSG_BOX_BG_ANIM:
				restartBoxAnim();
				break;
			case MSG_BOX_CLICK_ANIM:
				runClickAnimtion();
				break;
			}
		};
	};
	
	@Override
	public void onAnimationEnd(Animation arg0) {
		notifyRestartBoxAnim(0);
	}

	@Override
	public void onAnimationRepeat(Animation arg0) {
		
	}

	@Override
	public void onAnimationStart(Animation arg0) {
		
	}
}

动画类:
 
import android.content.Context;
import android.util.Log;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.ScaleAnimation;


public class AnimUtils {

	private static final String TAG = "AnimUtils";
	private static Animation mBoxAnimNormal;

	public static Animation buildAnimBoxNormal(Context context) {
		if (mBoxAnimNormal != null) {
			return mBoxAnimNormal;
		}
		mBoxAnimNormal = android.view.animation.AnimationUtils.loadAnimation(
				context, R.anim.box_alpha);
		return mBoxAnimNormal;
	}

	public static AnimationSet buildAnimBoxClick(Context context) {
		final ScaleAnimation scale = new ScaleAnimation(0.5f, 1.3f, 0.5f, 1.3f,
				Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
				0.5f);
		AlphaAnimation alpha = new AlphaAnimation(0.5f, 1.0f);
		AnimationSet mBoxAnimClick = new AnimationSet(true);
		mBoxAnimClick.addAnimation(scale);
		mBoxAnimClick.addAnimation(alpha);
		mBoxAnimClick.setDuration(100);
		return mBoxAnimClick;
	}
}


动画xml:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" 
    >
    <alpha
        android:repeatCount="infinite"
        android:repeatMode="restart"
        android:duration="1000"
        android:fromAlpha="2.0"
        android:toAlpha="0.1" />
    <alpha
         android:repeatCount="infinite"
        android:repeatMode="restart"
        android:duration="1000"
        android:fromAlpha="0.1"
        android:toAlpha="2.0" />
</set>


 

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false" >

    <item
        android:drawable="@drawable/bound"
        android:duration="1000"/>
    <item
        android:drawable="@drawable/bound2"
        android:duration="1000"/>

</animation-list>


bound:

bound2:
 

加入到布局中方法:

 父布局使用相对布局

<com.bestv.setting.views.BorderView
	    android:layout_width="0px"
	    android:layout_height="0px"
	    android:id="@+id/box"
	    />

在Acitivity中的使用方法:

import android.app.Activity;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;

import com.bestv.setting.utils.BoxNotFoundException;
import com.bestv.setting.views.BorderView;

public class BaseActivity extends Activity implements OnFocusChangeListener {
	
	private static final String TAG_base = "BaseActivity";
	BorderView mBorderView;
	Handler mHandler = new Handler();
	
	@Override
	protected void onStart() {
		super.onStart();
		View view = findViewById(R.id.box);
		if(view == null){
			throw new BoxNotFoundException();//必须在父布局中焦点框控件,否则抛出异常
		}
	}
	
	public BorderView getBorderView() {
		return mBorderView;
	}

	public void setBorderView(BorderView box) {
		this.mBorderView = box;
	}

	public void setFocusedView(final View view, int delay){
		if(mBorderView == null){
			mBorderView = (BorderView)findViewById(R.id.box);
		}
		mBorderView.runBorderAnimation();
		mHandler.postDelayed(new Runnable() {
			
			@Override
			public void run() {
				if(view == null){
					return;
				}
				view.requestFocus();
				mBorderView.setLocation(view);
			}
		}, delay);
	}
	
	public void runClickAnim(){
		this.getBorderView().notifyClickBoxAnim();
	}
	
	@Override
	protected void onResume() {
		super.onResume();
	}
	

	@Override
	public void onFocusChange(View v, boolean hasFocus) {
		if(hasFocus){
			mBorderView.runTranslateAnimation(v);
		}
	}
	public void setClickListener(View v, OnClickListener listener){
		v.setOnClickListener(listener);
		v.setOnFocusChangeListener(this);
	}
	
}


为每个能够获取到焦点的空间调用函数:

setClickListener(View v, OnClickListener listener)

 

至此全部完成。

Android中,遥控器焦点通常是指当用户使用遥控器进行导航时,屏幕上显示的高亮区域,它表明了用户当前选择的焦点焦点的显示主要依赖于Android焦点机制,开发者可以通过设置视图的焦点属性来控制焦点的显示。 编写遥控器焦点的基本步骤如下: 1. 在布局文件中定义可聚焦的视图,例如使用`ImageButton`、`TextView`等,这些视图默认是可聚焦的。 ```xml <ImageButton android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_button1" android:focusable="true" /> ``` 2. 在Activity或者Fragment的代码中设置焦点改变的监听器,根据焦点变化来调整视图属性,比如改变图片或者背景,以显示焦点。 ```java View button1 = findViewById(R.id.button1); button1.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { // 焦点在此视图上,显示焦点 v.setBackgroundResource(R.drawable.buttonFocused); } else { // 焦点离开此视图,取消焦点 v.setBackgroundResource(R.drawable.buttonNormal); } } }); ``` 3. 也可以在XML布局文件中直接使用`android:background`属性来设置焦点时的背景,并通过`android:state_focused`状态来定义焦点状态下的样式。 ```xml <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="true" android:drawable="@drawable/buttonFocused" /> <item android:drawable="@drawable/buttonNormal" /> </selector> ``` 4. 确保视图可以通过遥控器的导航键进行焦点变化,可能需要在Activity中重写`onGenericMotionEvent`方法来处理遥控器事件。 5. 在Android 5.0以上版本中,建议使用`ViewOutlineProvider`来自定义视图的轮廓,以创建更高级的视觉效果。 ```java button1.setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { // 定义焦点的形状和大小 outline.setRect(0, 0, button1.getWidth(), button1.getHeight()); } }); ``` 在实现遥控器焦点时,还需要考虑无障碍性和用户体验,确保焦点变化时的视觉反馈明显且符合用户习惯。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值