最近有朋友在做视频播放器,跟我提到了在做控制器的时候感觉逻辑和动画有一定嗯复杂性,让我一下子有了兴趣。
下图为实现的效果:
做这么一个功能,要考虑的内容大概分为以下几种:
1.界面上能看到的视图;
2.动画效果的实现;
3.控制器的收起、弹出相关的控制逻辑。
分析:
1.demo 中虽然只有上下两个控制器,但实际应用中也许左右也有,甚至中间也有,但无论如何他们都有一个共性:弹出、收起;
2.我朋友遇到的困难大概在于他所使用的 view 动画在频繁的点击事件下很难保证流畅性与连续性。并且熟悉 Android 动画的朋友应该知道,view 动画不会真正移动 view 的位置,也就是说他不得不对动画加上监听,去动态控制 view 上子 view 的点击事件是否有效。综上,在此处采用属性动画应该会更合适;
3.在 demo 中点击一次屏幕会弹出控制器,再点击会收起,弹出两秒左右之后还会自动收起。并且在动画执行过程中再次点击,会取消当前动画并反向执行。这个逻辑也许会根据业务的需求发生变化,所以应当尽可能只写在一处。
分享一下我实现的代码:
/**
* Controller 的定义
* 就是说你至少得可以 显示/隐藏 才能称为 Controller 对吧?
*/
public interface Controller {
void show();
void hide();
}
/**
* 采用 ValueAnimator 实现动画效果的基类 Controller
*/
public abstract class ValueAnimatorController implements Controller {
private static final int DURATION = 250;
/**
* 子类提供显示时的目标 value
* @return
*/
protected abstract int getShowTarget();
/**
* 子类提供隐藏时的目标 value
* @return
*/
protected abstract int getHideTarget();
/**
* 子类处理动态计算出的 value 以实现动画效果
* @param shift
*/
protected abstract void onShiftChanged(int shift);
protected View view;
protected ValueAnimator va;
protected int shift;
public ValueAnimatorController(View view) {
this.view = view;
}
@Override
public void show() {
stop();
makeAnimation(getShowTarget());
}
@Override
public void hide() {
stop();
makeAnimation(getHideTarget());
}
private void stop() {
if (va != null) {
va.cancel();
}
}
protected void makeAnimation(int target) {
//这里采用当前状态的 shift 而不是初始值,
//是为了动画被停止后,朝反方向移动更平滑
va = ValueAnimator.ofInt(shift, target);
va.setDuration(DURATION);
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
shift = (int) animation.getAnimatedValue();
onShiftChanged(shift);
}
});
va.start();
}
}
/**
* 出入事件分发及逻辑控制
*/
public class ControllerManager {
/**
* true 当前处于显示状态,或正在执行显示动画
* false 当前处于隐藏状态,或正在执行隐藏动画
*/
private boolean showing = true;
private List<Controller> controllerList = new ArrayList<>();
private CountDownTimer countDownTimer;
public ControllerManager addController(Controller controller) {
controllerList.add(controller);
return this;
}
/**
* 初始化完成后 开始倒计时隐藏 controllers
* @return
*/
public ControllerManager startWorking() {
startCount();
return this;
}
public boolean isShowing() {
return showing;
}
/**
* 切换状态
* 同时取消倒计时
*/
public void switchState() {
stopCount();
showing = !showing;
if (showing) {
show();
} else {
hide();
}
}
/**
* 分发 show 事件至所有 controller
* 同时开始倒计时
*/
private void show() {
for (Controller controller : controllerList) {
controller.show();
}
startCount();
}
private void hide() {
for (Controller controller : controllerList) {
controller.hide();
}
}
/**
* 倒计时结束时切换状态
*/
private void startCount() {
countDownTimer = new CountDownTimer(2500, 2500) {
@Override
public void onTick(long millisUntilFinished) {
}
@Override
public void onFinish() {
switchState();
}
}.start();
}
private void stopCount() {
if (countDownTimer != null) {
countDownTimer.cancel();
countDownTimer = null;
}
}
}
以上代码分别对应问题1、2、3。
实际使用方法如下:
1.先继承 ValueAnimatorController 实现 Top 和 Bottom 两种动画
public class TopController extends ValueAnimatorController {
public TopController(View view) {
super(view);
}
@Override
protected int getShowTarget() {
return 0;
}
@Override
protected int getHideTarget() {
return view.getHeight();
}
@Override
protected void onShiftChanged(int shift) {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams();
params.setMargins(params.leftMargin, -shift, params.rightMargin, params.bottomMargin);
view.setLayoutParams(params);
}
}
public class BottomController extends ValueAnimatorController {
public BottomController(View view) {
super(view);
}
@Override
protected int getShowTarget() {
return 0;
}
@Override
protected int getHideTarget() {
return view.getHeight();
}
@Override
protected void onShiftChanged(int shift) {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams();
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin, -shift);
view.setLayoutParams(params);
}
}
public class ControllersView extends RelativeLayout {
private Context context;
private View topView;
private View bottomView;
private ControllerManager controllerManager;
public ControllersView(Context context) {
super(context);
this.context = context;
initView();
}
private void initView() {
View rootView = LayoutInflater.from(context).inflate(R.layout.layout_control, this);
topView = rootView.findViewById(R.id.ll_top);
bottomView = rootView.findViewById(R.id.rl_bottom);
controllerManager = new ControllerManager();
controllerManager.addController(new TopController(topView))
.addController(new BottomController(bottomView))
.startWorking();
rootView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
controllerManager.switchState();
}
});
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new ControllersView(this));
}
}
扩展思路:
1.假如界面正中会出现一个按钮,出现时一边旋转一边变大,消失时反之,则在 ValueAnimator 的基础上新建一个 CenterController,重写 getShowTarget, getHideTarget, onShiftChange 三个方法,再将按钮的 view 和此 controller 绑定再提交至 controllerManager 即可;
2.如果界面上有多种不同的逻辑出现,比如说上述1中按钮需要双击才能消失,那么需要重写一种 ControllerManager,实现不同的逻辑。即在自定义的View中,根据逻辑的不同,会同时存在多个不同的 ControllerManager,用来管理逻辑不同的 view;
3.如果不想使用 ValueAnimator 来实现动画效果,可以自行写一个实现了 Controller 接口所描述方法的类。
项目源码: