Android可拖動時間刻度軸

先看效果

效果圖.png

可左右拖動和通過縮放來實現不同刻度模式的切換,下面代碼只使用了兩種刻度模式.
綠色部分代表有錄制的視頻數據,該效果可根據需求自行修改.

iOS版
寫法思路完全相同

思路

繪圖主要繪制刻度線和時間數值,其中要記錄和使用的主要數值為時間戳,相應時間戳在需要繪制的界面里對應的x的值,時間參考為時間軸中線,而刻度線參考為0刻度即左邊界.需要處理的主要關系為時間戳和中間刻度所代表的時間刻度改變時各個時間戳所代表的相對于控件本身的x的值.
那么所有數據都可以通過中間線位置所代表的時間,控件本身的寬度,刻度線間距寬度和其所代表的時間長度進行換算和相互轉化來解決,而繪圖為保證性能可以通過計算只去繪制需要繪制的內容.
最終需要達到的效果是通過改變中央刻度線所代表的時間戳的值來實現整個時間軸的繪制.而我們改變時間軸就只要改變中間刻度線所代表的時間戳,然后重新繪制就可以實現各種效果,比如拖動,就是根據手指拖動的距離換算成時間長度對中央刻度線時間戳進行加減,然后不斷繪制達到拖動時整個時間軸滑動的效果.

代碼
public class ZFTimeLine extends View {

    private final int SCALE_TYPE_BIG = 1;           //大刻度
    private final int SCALE_TYPE_SMALL = 2;         //小刻度

    private int intervalValue;                    //小刻度寬度
    private int scaleType;
    private long currentInterval;                  //中間刻度對應的時間戳

    private SimpleDateFormat formatterScale;        //日期格式化,用于時間戳和時間字符的轉換
    private SimpleDateFormat formatterProject;      //日期格式化,用于時間戳和時間字符的轉換

    private Paint paintWhite,paintGreen,paintRed;   //三種不同顏色的畫筆
    private int point = 0;                          //用于當前觸控點數量
    private float moveStartX = 0;                   //用于記錄單點觸摸點位置,用于計算拖距離
    private float scaleValue = 0;                   //用于記錄兩個觸摸點間距,用于時間軸縮放計算

    private boolean onLock;                         //用于屏蔽時間軸拖動,為true時無法拖動

    private OnZFTimeLineListener listener;          //時間軸拖動監聽,這個只在拖動完成時返回數據

    //已錄制視頻數據信息
    List<VideoInfo> calstuff;
    //設置監聽
    public void setListener(OnZFTimeLineListener listener) {
        this.listener = listener;
    }

    //拖動時間軸監聽
    public interface OnZFTimeLineListener{
        void didMoveToDate(String date);
    }
    public ZFTimeLine(Context context) {
        super(context);
        init();
    }

    public ZFTimeLine(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ZFTimeLine(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    //數據數據初始化
    private void init(){

        scaleType = SCALE_TYPE_BIG;
        intervalValue = 0;
        setAlpha(0.8f);
        setBackgroundColor(Color.BLACK);
        timeNow();

        onLock = false;

        formatterScale = new SimpleDateFormat("HH:mm");
        formatterProject = new SimpleDateFormat("yyyyMMddHHmmss");

        paintWhite = new Paint();
        paintWhite.setColor(Color.WHITE);
        paintWhite.setTextSize(intDip2px(10));
        paintWhite.setTextAlign(Paint.Align.CENTER);
        paintWhite.setStrokeWidth(dip2px(0.5f));

        paintGreen = new Paint();
        paintGreen.setColor(Color.GREEN);

        paintRed = new Paint();
        paintRed.setColor(Color.RED);
    }

    //把當前時間戳設置我中間刻度對應的時間戳
    private void timeNow(){
        currentInterval = System.currentTimeMillis();
    }
    //寬度1所代表的毫秒數
    private long milliscondsOfIntervalValue(){
        if (scaleType == SCALE_TYPE_BIG){
            return (long) (6*60000.0/intervalValue);
        }else {
            return (long) (60000.0/intervalValue);
        }
    }

    private float dip2px(float dipValue){
        return dipValue * (getResources().getDisplayMetrics().densityDpi / 160);
    }
    private int intDip2px(float dipValue){
        return (int) (dip2px(dipValue) + 0.5);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //初始化小刻度的間隔,在init里densityDpi的數據為0,所以放到這里了
        if (intervalValue == 0) intervalValue = intDip2px(10);

        //中間線的x值
        long centerX = getWidth()/2;
        //左邊界線代表的時間戳
        long leftInterval = currentInterval - centerX * milliscondsOfIntervalValue();
        //右邊界線時間戳
        long rightInterval = currentInterval + centerX * milliscondsOfIntervalValue();

        long x;             //記錄繪制刻度線的位置
        long interval;      //記錄所繪制刻度線代表的時間戳

        //下面計算需要繪制的第一個刻度線的位置和所代表的時間戳
        if (scaleType == SCALE_TYPE_BIG){
            long a = leftInterval/(60 * 6 * 1000);
            interval =  ((a + 1) * (60 * 6 * 1000));
            x = (interval - leftInterval) / milliscondsOfIntervalValue();
        }else {
            long a = leftInterval/(60 * 1000);
            interval = ((a + 1) * (60 * 1000));
            x = (interval - leftInterval) / milliscondsOfIntervalValue();
        }

        //這里是這個項目特有的需求,根據視頻數據繪制綠色和紅色區域,分別代表該位置有已錄制的普通視頻和緊急視頻(行車記錄儀)
        if (calstuff != null){
            for (int i = 0;i<calstuff.size();i++){
                VideoInfo info = calstuff.get(i);
                //獲取視頻文件的開始時間戳和結束時間戳
                long startInterval = info.getStartTime().getTimeInMillis();
                long endInterval = info.getEndTime().getTimeInMillis();
                //判斷是否需要繪制
                if ((startInterval > leftInterval && startInterval < rightInterval)
                        || (endInterval > leftInterval && endInterval < rightInterval)
                        || (startInterval < leftInterval && endInterval > rightInterval)){
                    //將開始和結束時間戳轉化為對應的x的位置
                    long startX = (startInterval - leftInterval)/milliscondsOfIntervalValue();
                    long endX = (endInterval - leftInterval)/milliscondsOfIntervalValue();
                    if (info.getFileName().contains("SOS")){
                        //緊急視頻 為紅色區域色塊
                        canvas.drawRect(startX,0,endX,getHeight()-dip2px(24),paintRed);
                    }else {
                        //普通的為綠色
                        canvas.drawRect(startX,0,endX,getHeight()-dip2px(24),paintGreen);
                    }
                }

//                Dbug.e("====>", "" + info.getStartTime().getTimeInMillis());
            }
        }
        //畫刻度線
        while (x >= 0 && x<= getWidth()){
            int a;          //長刻度線間隔所代表的時間長度,用于計算,單位是毫秒
            if (scaleType == SCALE_TYPE_BIG){
                a= 60000 * 6;
            }else {
                a = 60000;
            }
            long rem = interval % (a * 5);
            //根據時間戳值對大刻度間隔是否整除判斷畫長刻度或者短刻度
            if (rem != 0){//小刻度
                canvas.drawLine(x,getHeight() - dip2px(5),x,getHeight(),paintWhite);
            }else {//大刻度
                canvas.drawLine(x,getHeight() - dip2px(10),x,getHeight(),paintWhite);
                //大刻度繪制時間文字
                String time = formatterScale.format(interval);
                canvas.drawText(time,x,getHeight() - dip2px(12),paintWhite);
            }
            //下一個刻度
            x = x + intervalValue;
            interval = interval + a;
        }
        //畫中間線
        canvas.drawLine(centerX,0,centerX,getHeight(),paintWhite);
    }

    //通過onTouchEvent來實現拖動和縮放
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction() & MotionEvent.ACTION_MASK){
            case MotionEvent.ACTION_DOWN:{
//                Log.e("touch","ACTION_DOWN" + event.getX());
                point = 1;
                moveStartX = event.getX();
            }break;
            case MotionEvent.ACTION_POINTER_DOWN:{
//                Log.e("touch","ACTION_POINTER_DOWN" + event.getX(0) + "-----" + event.getX(1));
                point = point + 1;
                if (point == 2){
                    scaleValue = Math.abs(event.getX(1) - event.getX(0));
                }
            }break;
            case MotionEvent.ACTION_MOVE:{
//                Log.e("touch","ACTION_MOVE");
                if (point == 1){
                    //拖動
                    currentInterval = currentInterval - milliscondsOfIntervalValue() * ((long) (event.getX() -
                            moveStartX));
                    moveStartX = event.getX();
                }else if (point == 2){
                    float value = Math.abs(event.getX(1) - event.getX(0));

                    if (scaleType == SCALE_TYPE_BIG){
                        if (scaleValue - value < 0){//變大
                            intervalValue = intervalValue + ((int) ((value - scaleValue)/dip2px(100)));
                            if (intervalValue >= intDip2px(15)){
                                scaleType = SCALE_TYPE_SMALL;
                                intervalValue = intDip2px(10);
                            }
                        }else {//變小
                            intervalValue = intervalValue + ((int) ((value - scaleValue)/dip2px(100)));
                            if (intervalValue < intDip2px(10)){
                                intervalValue = intDip2px(10);
                            }
                        }
                    }else {
                        if (scaleValue - value < 0){//變大
                            intervalValue = intervalValue + ((int) ((value - scaleValue)/dip2px(100)));
                            if (intervalValue >= intDip2px(15)){
                                intervalValue = intDip2px(15);
                            }
                        }else {//變小
                            intervalValue = intervalValue + ((int) ((value - scaleValue)/dip2px(100)));
                            if (intervalValue < intDip2px(10)){
                                scaleType = SCALE_TYPE_BIG;
                                intervalValue = intDip2px(10);
                            }
                        }
                    }
                }else {
                    return true;
                }
            }break;
            case MotionEvent.ACTION_POINTER_UP:{
//                Log.e("touch","ACTION_POINTER_UP");
                point = point - 1;
            }break;
            case MotionEvent.ACTION_UP:{
//                Log.e("touch","ACTION_UP");
                point = 0;
                //拖動結束  這里應該有Bug沒有區分移動可縮放狀態 不過影響不大
                if (listener != null){
                    listener.didMoveToDate(formatterProject.format(currentInterval));
                }
            }break;
        }
        //重新繪制
        invalidate();
        return true;
    }

    //所有暴露的刷新方法使用不當會引起崩潰(在時間軸創建之后但是沒有顯示的時候調用),解決辦法是使用handel來調用該方法
    //刷新,重新繪制
    public void refresh(){
        invalidate();
    }

    //刷新到當前時間
    public void refreshNow(){
        if (onLock || point != 0){
            return;
        }
        timeNow();
        refresh();
    }

    //移動到某時間  傳入參數格式舉例 20170918120000
    public void moveTodate(String timeStr){
        if (onLock || point != 0){
            return;
        }
        try {
            currentInterval = formatterProject.parse(timeStr).getTime();
            invalidate();
            if (listener != null){
                listener.didMoveToDate(formatterProject.format(currentInterval));
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
    //移動到某時間 傳入時間戳
    public void moveTodate(long timeInterval){
        if (onLock || point != 0){
            return;
        }
        if (timeInterval == 0)return;
        currentInterval = timeInterval;
        invalidate();
    }

    //獲取當前時間軸指向的時間 返回參數格式舉例 20170918120000
    public String currentTimeStr(){
        return formatterProject.format(currentInterval);
    }

    //鎖定,不可拖動和縮放
    public void lockMove(){
        onLock = true;
    }

    //解鎖,可以拖動和縮放
    public void unLockMove(){
        onLock = false;
    }

    //獲取當前時間軸指向的時間的時間戳
    public long getCurrentInterval(){
        return currentInterval;
    }
    //把時間數據轉化為時間戳
    public long timeIntervalFromStr(String str){
        try {
            return formatterProject.parse(str).getTime();
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return 0;
    }
    //把時間戳轉化為時間字符串
    public String timeStrFromInterval(long interval){
        return formatterProject.format(interval);
    }

    //寫入視頻數據
    public void setCalstuff(List<VideoInfo> mcalstuff) {
        this.calstuff = mcalstuff;
        refresh();
    }

    //清除視頻信息
    public void clearVideoInfos(){
        this.calstuff = null;
        refresh();
    }
}
使用
        <com.gushang.ZFTimeLine
            android:id="@+id/scalePanel"
            android:layout_alignParentBottom="true"
            android:background="@android:color/transparent"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        </com.gushang.ZFTimeLine>

因為項目用到,而且這種效果的資料不太好找,所以在解決之后記錄和分享一下.
也方便我以后處理繪圖需求時用作參考.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容

  • 效果 可左右拖動和通過縮放來實現不同刻度模式的切換,下面代碼只使用了兩種刻度模式.綠色部分代表有錄制的視頻數據,該...
    夢里風吹過閱讀 4,551評論 8 8
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,693評論 25 708
  • Matplotlib 入門教程 來源:Introduction to Matplotlib and basic l...
    布客飛龍閱讀 31,859評論 5 162
  • 大家對優惠券應該都比較熟悉,有過網購經驗的人應該都用過,優惠券也是商家促銷一種常用的手段,今天來跟大家介紹下B2C...
    游社長閱讀 839評論 0 16
  • 第三天 下一天清晨,我將再一次迎接黎明,急于尋找新的喜...
    聚錦緣閱讀 635評論 0 0