Android 視訊控制器出入邏輯及動畫的封裝

neverwoods發表於2017-12-23

最近有朋友在做視訊播放器,跟我提到了在做控制器的時候感覺邏輯和動畫有一定嗯複雜性,讓我一下子有了興趣。 下圖為實現的效果:

video-controller-animation.gif

做這麼一個功能,要考慮的內容大概分為以下幾種: 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

相關文章