最近有朋友在做視訊播放器,跟我提到了在做控制器的時候感覺邏輯和動畫有一定嗯複雜性,讓我一下子有了興趣。 下圖為實現的效果:
做這麼一個功能,要考慮的內容大概分為以下幾種: 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);
}
}
複製程式碼
2.將實際的 view 傳入至 controller 中,再將 controller 統一交由 ControllerManager 統一處理事件
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();
}
});
}
}
複製程式碼
擴充套件思路
1.假如介面正中會出現一個按鈕,出現時一邊旋轉一邊變大,消失時反之,則在 ValueAnimator 的基礎上新建一個 CenterController,重寫 getShowTarget, getHideTarget, onShiftChange 三個方法,再將按鈕的 view 和此 controller 繫結再提交至 controllerManager 即可;
2.如果介面上有多種不同的邏輯出現,比如說上述1中按鈕需要雙擊才能消失,那麼需要重寫一種 ControllerManager,實現不同的邏輯。即在自定義的View中,根據邏輯的不同,會同時存在多個不同的 ControllerManager,用來管理邏輯不同的 view;
3.如果不想使用 ValueAnimator 來實現動畫效果,可以自行寫一個實現了 Controller 介面所描述方法的類。
專案原始碼
https://github.com/neverwoodsS/VideoController