自定義View 實現(xiàn)簽到效果

最近公司有一個需求是關(guān)于簽到方面的效果静稻,想著自己對自定義View這塊不是很熟悉睹耐,所以就想著自己動手來實現(xiàn)下糊探,以此來學(xué)習(xí)下自定義View。
首先來一張自己實現(xiàn)的效果圖

img.jpg

GitHub
引用

implementation 'com.lishang:checkInProgress:1.0.1'
屬性 類型 描述
text_date_size sp 日期文字大小
text_date_color color 日期文字顏色
radius dp 簽到圓半徑
circle_color color 圓的背景色
line_height dp 線高
line_color color 線的顏色
text_score_size sp 簽到積分字體大小
text_score_color color 簽到積分文字顏色
check_in_bitmap drawble 簽到后的圖片
check_in_color color 沒有簽到圖片時扣汪,簽到的顏色
check_in_hook_color color 沒有簽到圖片時断楷,簽到內(nèi)部勾的顏色
check_in_hook_size dp 沒有簽到圖片時,簽到內(nèi)部勾的大小
circle_margin dp 簽到圓頂部與日期字體距離
circle_stroke_width dp 簽到圓描邊寬度
circle_stroke_color color 簽到圓邊描顏色
check_in_progress_show boolean 是否顯示簽到進度
check_in_progress_color color 簽到進度顏色
check_in_leak_show boolean 是否支持補簽
circle_style enum 簽到圓樣式 fill 填充 stroke描邊(circle_stroke_width崭别、circle_stroke_color生效)
align enum 位置 top/center/bottom

使用

  <com.lishang.checkin.CheckInProgress
    android:id="@+id/checkIn_1"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_gravity="center_horizontal"
    android:layout_marginTop="10dp"
    android:background="#408ce2"
    app:align="center"
    app:check_in_color="#ceebfd"
    app:check_in_hook_color="#2d66d9"
    app:check_in_leak_show="true"
    app:circle_color="#2d66d9"
    app:circle_margin="5dp"
    app:circle_stroke_color="#ceebfd"
    app:circle_stroke_width="1dp"
    app:circle_style="stroke"
    app:line_color="#2d66d9"
    app:line_height="1dp"
    app:radius="10dp"
    app:text_date_color="#edffff"
    app:text_date_size="12sp"
    app:text_score_color="#9bccff"
    app:text_score_size="12sp" />


checkIn.setAdapter(new CheckInProgress.Adapter() {
        /**
         * 日期
         * @param position
         * @return
         */
        @Override
        public String getDateText(int position) {
            CheckIn in = list.get(position);
            return in.date;
        }

        /**
         * 積分
         * @param position
         * @return
         */
        @Override
        public String getScoreText(int position) {
            CheckIn in = list.get(position);
            return in.score;
        }

        /**
         * 是否簽到
         * @param position
         * @return
         */
        @Override
        public boolean isCheckIn(int position) {
            CheckIn in = list.get(position);
            return in.isCheckIn;
        }

        /**
         * 數(shù)量
         * @return
         */
        @Override
        public int size() {
            return list.size();
        }

        /**
         * 是否支持補簽
         * @param position
         * @return
         */
        @Override
        public boolean isLeakCheckIn(int position) {
            CheckIn in = list.get(position);

            return in.isLeakChekIn;
        }
    });

checkIn.setOnClickCheckInListener(new                       
  OnClickCheckInListener() {
        @Override
        public void OnClick(int position) {
            CheckIn checkIn = list1.get(position);
            if (checkIn.isCheckIn) {
                Toast.makeText(getApplicationContext(), "已簽到", Toast.LENGTH_SHORT).show();
            } else {
                if (checkIn.isLeakChekIn) {
                    Toast.makeText(getApplicationContext(), "補卡", Toast.LENGTH_SHORT).show();
                    checkIn.isLeakChekIn = false;
                    checkIn.isCheckIn = true;

                    Log.e("CheckIn", Arrays.toString(list1.toArray()));

                    checkIn.getAdapter().notifyDataSetChanged();
                } else {
                    Toast.makeText(getApplicationContext(), "簽到", Toast.LENGTH_SHORT).show();
                }
            }
        }
    });

代碼簡要概括

自定義View冬筒,主要需要實現(xiàn)兩個方法:

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

主要用來測量當(dāng)前控件的寬高widthMeasureSpec和heightMeasureSpec這兩個值通常情況下都是由父視圖經(jīng)過計算后傳遞給子視圖的,說明父視圖會在一定程度上決定子視圖的大小茅主。

認(rèn)識 MeasureSpec

在測量自定義view的大小之前舞痰,我們需要認(rèn)識一個類MeasureSpec,它封裝了父布局傳遞給子布局的布局要求暗膜,每個MeasureSpec代表了一組寬度和高度的要求 MeasureSpec由size和mode組成匀奏。

specMode一共有三種類型,如下所示:
1. EXACTLY

表示父視圖希望子視圖的大小應(yīng)該是由specSize的值來決定的学搜,系統(tǒng)默認(rèn)會按照這個規(guī)則來設(shè)置子視圖的大小,簡單的說(當(dāng)設(shè)置width或height為match_parent時论衍,模式為EXACTLY瑞佩,因為子view會占據(jù)剩余容器的空間,所以它大小是確定的)

2. AT_MOST

表示子視圖最多只能是specSize中指定的大小坯台。(當(dāng)設(shè)置為wrap_content時炬丸,模式為AT_MOST, 表示子view的大小最多是多少,這樣子view會根據(jù)這個上限來設(shè)置自己的尺寸)

3. UNSPECIFIED

表示開發(fā)人員可以將視圖按照自己的意愿設(shè)置成任意的大小蜒蕾,沒有任何限制稠炬。這種情況比較少見,不太會用到咪啡。

onDraw

用來繪制View需要顯示的內(nèi)容

下面來看代碼

 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //計算元素位置
    onCalculation();

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
   
    //當(dāng)View的高是wrap_content 時首启,高度設(shè)置為實際測量的高度
    if (heightMode == MeasureSpec.AT_MOST && verticalHeight != 0) {
        heightSize = verticalHeight;
    }

    setMeasuredDimension(widthSize, heightSize);
}

onCalculation()方法用了測量View上各個元素的位置,并保存下來

   /**
 * 先計算好畫布上每個元素的位置
 */
private void onCalculation() {
    if (adapter == null) return;
    calculationDate();
    calculationScore();

    //元素垂直高度
    int total = datePointPool.get(0).y - getPaddingTop(); //日期的高度
    total += (circleMargin); // + 間距
    total += (radius) * 2; //+積分圓的直徑
    verticalHeight = total;

}

/**
 * 日期元素位置
 */
private void calculationDate() {
    int left = getPaddingLeft();
    int right = getPaddingRight();
    int top = getPaddingTop();

    int width = getMeasuredWidth() - left - right;


    int margin = width / (adapter.size());

    //日期位置
    int cy = 0;
    for (int i = 0; i < adapter.size(); i++) {

        String str = adapter.getDateText(i);
        Rect rect = new Rect();
        datePaint.getTextBounds(str, 0, str.length(), rect);
        int y = top + rect.height();
        if (cy < y) {
            cy = y;
        }
    }

    for (int i = 0; i < adapter.size(); i++) {
        int cx = left + margin / 2 + i * margin;
        Point point = new Point(cx, cy);
        datePointPool.put(i, point);
    }
}

/**
 * 積分元素位置
 */
private void calculationScore() {

    int radiusPx = (radius);
    int left = datePointPool.get(0).x;
    int right = datePointPool.get(datePointPool.size() - 1).x;
    int top = datePointPool.get(0).y;

    int width = right - left;
    int cy = top + radiusPx + (circleMargin);

    int margin = width / (adapter.size() - 1);
    for (int i = 0; i < adapter.size(); i++) {

        int cx = left + i * margin;
        Point p = new Point(cx, cy);
        circlePointPool.put(i, p);

        scorePaint.setTextSize((textScoreSize));
        scorePaint.setStyle(Paint.Style.FILL);
        scorePaint.setColor(textScoreColor);
        scorePaint.setTextAlign(Paint.Align.CENTER);
        String str = "+" + adapter.getScoreText(i);
        if (adapter.isLeakCheckIn(i) && checkInLeakShow) {
            str = "補";
        }
        Rect rect = new Rect();
        scorePaint.getTextBounds(str, 0, str.length(), rect);

        Paint.FontMetricsInt fontMetrics = scorePaint.getFontMetricsInt();

        Point point = new Point(p.x, p.y + (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent);
        scorePointPool.put(i, point);
    }

}

onDraw(Canvas canvas) 進行View內(nèi)部元素繪制

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    drawDate(canvas);
    drawBgLine(canvas);
    drawScore(canvas);
}
 /**
 * 畫日期
 *
 * @param canvas
 */
private void drawDate(Canvas canvas) {

    int margin = calculationAlign();
    if (datePointPool.size() != 0) {

        for (int i = 0; i < adapter.size(); i++) {

            String str = adapter.getDateText(i);

            datePaint.setColor(textDateColor);

            Point point = datePointPool.get(i);

            canvas.drawText(str, point.x, point.y + margin, datePaint);

        }

    }
}

private void drawBgLine(Canvas canvas) {
    int margin = calculationAlign();

    int startX = datePointPool.get(0).x;
    int startY = datePointPool.get(0).y + (radius) + (circleMargin) + margin;
    int stopX = datePointPool.get(datePointPool.size() - 1).x;
    int stopY = startY;
    canvas.drawLine(startX, startY, stopX, stopY, linePaint);


}

private void drawScore(Canvas canvas) {
    int radiusPx = (radius);
    int margin = calculationAlign();

    for (int i = 0; i < adapter.size(); i++) {
        Point p = circlePointPool.get(i);


        if (adapter.isCheckIn(i)) {

            if (checkInProgressShow && i + 1 < adapter.size()) {
                //進度
                scorePaint.setStyle(Paint.Style.FILL);
                scorePaint.setColor(checkInProgressColor);
                scorePaint.setStrokeWidth((lineHeight));

                Point p1 = circlePointPool.get(i + 1);
                canvas.drawLine(p.x, p.y + margin, p1.x, p1.y + margin, scorePaint);
            }

            if (checkIn != null) {
                float scale = radiusPx * 2.0f / checkIn.getWidth();
                Matrix matrix = new Matrix();
                matrix.postScale(scale, scale);
                canvas.save();
                canvas.translate(p.x - radiusPx, p.y + margin - radiusPx);
                canvas.drawBitmap(checkIn, matrix, scorePaint);
                canvas.restore();
            } else {
                scorePaint.setColor(checkInColor);
                scorePaint.setStyle(Paint.Style.FILL);
                canvas.drawCircle(p.x, p.y + margin, radiusPx, scorePaint);

                //畫勾
                scorePaint.setStyle(Paint.Style.FILL);
                scorePaint.setColor(checkInHookColor);
                scorePaint.setStrokeWidth((checkInHookSize));
                int startX = p.x - radiusPx / 4 * 3;
                int startY = p.y + margin;
                int stopX = p.x - radiusPx / 4;
                int stopY = p.y + margin + radiusPx / 2;
                canvas.drawLine(startX, startY, stopX, stopY, scorePaint);

                startX = stopX;
                startY = stopY;
                stopX = p.x + radiusPx / 4 * 3;
                stopY = p.y + margin - radiusPx / 2;
                canvas.drawLine(startX, startY, stopX, stopY, scorePaint);

                canvas.drawCircle(startX, startY, checkInHookSize / 2.0f, scorePaint);
            }


        } else {
            scorePaint.setStyle(Paint.Style.FILL);
            scorePaint.setColor(circleColor);
            canvas.drawCircle(p.x, p.y + margin, radiusPx, scorePaint);

            if (circleStyle == Paint.Style.STROKE) {
                scorePaint.setColor(circleStrokeColor);
                scorePaint.setStrokeWidth((circleStrokeWidth));
                scorePaint.setStyle(Paint.Style.STROKE);
                canvas.drawCircle(p.x, p.y + margin, radiusPx, scorePaint);
            }

            scorePaint.setTextSize((textScoreSize));
            scorePaint.setStyle(Paint.Style.FILL);
            scorePaint.setColor(textScoreColor);
            scorePaint.setTextAlign(Paint.Align.CENTER);
            String str = "+" + adapter.getScoreText(i);
            if (adapter.isLeakCheckIn(i) && checkInLeakShow) {
                str = "補";
            }

            Point point = scorePointPool.get(i);

            canvas.drawText(str, point.x, point.y + margin, scorePaint);
        }

    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末撤摸,一起剝皮案震驚了整個濱河市毅桃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌准夷,老刑警劉巖钥飞,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異衫嵌,居然都是意外死亡读宙,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門楔绞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來结闸,“玉大人掖棉,你說我怎么就攤上這事“蚬溃” “怎么了幔亥?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長察纯。 經(jīng)常有香客問我帕棉,道長,這世上最難降的妖魔是什么饼记? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任香伴,我火速辦了婚禮,結(jié)果婚禮上具则,老公的妹妹穿的比我還像新娘即纲。我一直安慰自己,他們只是感情好博肋,可當(dāng)我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布低斋。 她就那樣靜靜地躺著,像睡著了一般匪凡。 火紅的嫁衣襯著肌膚如雪膊畴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天病游,我揣著相機與錄音唇跨,去河邊找鬼。 笑死衬衬,一個胖子當(dāng)著我的面吹牛买猖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播滋尉,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼玉控,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了兼砖?” 一聲冷哼從身側(cè)響起奸远,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎讽挟,沒想到半個月后懒叛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡耽梅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年薛窥,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡诅迷,死狀恐怖佩番,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情罢杉,我是刑警寧澤趟畏,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站滩租,受9級特大地震影響赋秀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜律想,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一猎莲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧技即,春花似錦著洼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至澈歉,卻和暖如春展鸡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背埃难。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留涤久,地道東北人涡尘。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像响迂,于是被迫代替她去往敵國和親考抄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,440評論 2 359