自定義View之FormView

本Demo主要目的為學(xué)習(xí)及研究自定義View,通過(guò)實(shí)現(xiàn)一個(gè)圖表的數(shù)據(jù)展示功能舔哪,熟悉和了解View的繪制過(guò)程

先看一下產(chǎn)品需求

產(chǎn)品需求
產(chǎn)品需求
  • X軸和Y軸坐標(biāo)分別表示時(shí)間及對(duì)應(yīng)的數(shù)值
  • Y軸坐標(biāo)依數(shù)據(jù)顯示5-10行攻走,Y軸輔助線顯示3-5條
  • X軸依時(shí)間文字的長(zhǎng)短進(jìn)行展示,要求X軸坐標(biāo)值不重合
  • 各坐標(biāo)點(diǎn)用直線相連嚎花,且連線與X軸區(qū)域添加漸變色
  • 添加touch時(shí)間,當(dāng)觸摸至坐標(biāo)點(diǎn)時(shí)顯示文本框,顯示說(shuō)明文本按咒,繪制坐標(biāo)點(diǎn)圓圈及X、Y軸輔助線

功能分析

為完成產(chǎn)品的需求仓手,我們需要解決如下的6個(gè)問(wèn)題:
1.首先胖齐,我們需要計(jì)算出繪圖區(qū)域及坐標(biāo)軸文字顯示區(qū)域玻淑;
2.繪制坐標(biāo)軸文字;
3.繪制平行于X軸的輔助線呀伙;
4.計(jì)算各坐標(biāo)點(diǎn)位置补履;
5.連接各坐標(biāo)點(diǎn)并繪制漸變區(qū)域;
6.捕捉touch事件并添加回調(diào)剿另;
7.依據(jù)回調(diào)設(shè)置提示框內(nèi)容并繪制提示框箫锤。

代碼實(shí)現(xiàn)

因?yàn)橐故緮?shù)據(jù),所以需要自定義View暴露對(duì)外的設(shè)置數(shù)據(jù)的接口雨女,同時(shí)數(shù)據(jù)需要如下三個(gè)屬性:顏色(繪制連接線時(shí)連接線的顏色)谚攒、Y軸坐標(biāo)(選用string類型,因?yàn)闄M坐標(biāo)可能是周一氛堕、二……)馏臭、X軸坐標(biāo)值(這里選用double類型)。因此讼稚,在自定義View中可以使用內(nèi)部類Units來(lái)作為坐標(biāo)點(diǎn)括儒,同時(shí)用Map<Color,Units>來(lái)保存需要展示的數(shù)據(jù)。

//坐標(biāo)點(diǎn)位置
public static class Units {
    public double y;
    String x;

    public Units(double y, String x) {
        this.x = x;
        this.y = y;
    }
}
//對(duì)外暴露的接口锐想,用以設(shè)置數(shù)據(jù)
public void resetData(Map<Integer, List<Units>> map) {
    this.mDatas.clear();
    Iterator<Integer> it = map.keySet().iterator();
    while (it.hasNext()) {
        Integer color = it.next();
        mDatas.put(color, map.get(color));
    }
    invalidate();
}

我們知道帮寻,因?yàn)槭亲远xView,所以我們需要添加一些atrrs屬性赠摇,便于對(duì)View進(jìn)行一些設(shè)置固逗;
本demo中添加的一些屬性如下

屬性名 類型 說(shuō)明
min_size integer view最小尺寸
base_stroke_width integer 基礎(chǔ)線條寬度
base_stroke_color color 基礎(chǔ)線條顏色
base_text_size integer 坐標(biāo)文字大小
help_text_size integer 彈出提示框文字大小
help_text_margin integer 彈出提示框Margin
text_margin_y integer Y方向文字與表格間距
point_size integer 觸摸時(shí)顯示坐標(biāo)點(diǎn)的大小
point_touch_size integer 觸摸范圍
text_margin_x integer X方向文字與表格間距
zero_start boolean Y軸是否從零開(kāi)始
help_text_bg_res reference 觸摸響應(yīng)說(shuō)明背景
shader boolean 是否添加X(jué)坐標(biāo)與連線間的漸變

有了如上屬性,我們?cè)谧远x初始化的初始化這些屬性,同時(shí)初始化Paint

private void init(AttributeSet attrs) {
    TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.RouteeFormView);
    mMinSize = a.getInteger(R.styleable.RouteeFormView_min_size, 0);
    mBaseColor = a.getColor(R.styleable.RouteeFormView_base_stroke_color, Color.parseColor("#d0d0d0"));
    mBaseStrokeWidth = a.getInteger(R.styleable.RouteeFormView_base_stroke_width, 1);
    mBaseTextSize = a.getInteger(R.styleable.RouteeFormView_base_text_size, 12);
    mHelpTextSize = a.getInteger(R.styleable.RouteeFormView_help_text_size, 14);
    mHelpTextMargin = a.getInteger(R.styleable.RouteeFormView_help_text_margin, 8);
    mTextMarginX = DisplayUtils.dp2px(getContext(), a.getInteger(R.styleable.RouteeFormView_text_margin_x, 4));
    mTextMarginY = DisplayUtils.dp2px(getContext(), a.getInteger(R.styleable.RouteeFormView_text_margin_y, 4));
    mHelpTextBgResId = a.getResourceId(R.styleable.RouteeFormView_help_text_bg_res, R.drawable.bg_routee_form_view_help_text);
    mNeedDrawShader = a.getBoolean(R.styleable.RouteeFormView_shader, false);
    mPointWidth = DisplayUtils.dp2px(getContext(), a.getInteger(R.styleable.RouteeFormView_point_size, 2));
    mPointTouchWith = DisplayUtils.dp2px(getContext(), a.getInteger(R.styleable.RouteeFormView_point_touch_size, 10));
    isStartZero = a.getBoolean(R.styleable.RouteeFormView_zero_start, false);
    a.recycle();
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
}

然后藕帜,我們需要重寫(xiě)我們的onMeasure方法,計(jì)算自定義View的大小

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    if (widthSpecMode == AT_MOST && heightSpecMode == AT_MOST) {
        setMeasuredDimension(mMinSize, mMinSize);
    } else if (widthMeasureSpec == AT_MOST) {
        setMeasuredDimension(mMinSize, heightSpecSize);
    } else if (heightMeasureSpec == AT_MOST) {
        setMeasuredDimension(widthSpecSize, mMinSize);
    }
}

在計(jì)算出View的尺寸后烫罩,我們需要開(kāi)始完成自定View最重要的一步繪制,也就是重寫(xiě)onDraw(Canvas canvas)方法,依據(jù)需求分析洽故,我們需要進(jìn)行一些列的計(jì)算再去按如下順序去繪制View的不同部分:

流程圖
流程圖
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //當(dāng)設(shè)置的數(shù)據(jù)為空時(shí)不繪制任何UI
    if (mDatas == null || mDatas.size() == 0) {
        return;
    }
    calc();
    drawText(canvas);
    drawLines(canvas);
    drawData(canvas);
    drawHelpLine(canvas);
    drawHelpText(canvas);
}

在上述的onDraw(Canvas canvas)我們發(fā)現(xiàn)嗡髓,在drawText(cavas)繪制坐標(biāo)軸文字之前我們執(zhí)行了calc()方法,該方法其實(shí)就是我們之前需求分析時(shí)提到的需要計(jì)算的一些東西收津。我們?cè)賮?lái)看看calc()都計(jì)算了哪些:

private void calc() {
    calcMaxYValue();    //計(jì)算Y軸最大值
    calcMinYValue();    //計(jì)算Y軸最小值
    calcYSpacing();     //計(jì)算Y軸間隔大小
    calcYTextList();    //計(jì)算Y軸的文字內(nèi)容列表
    calcTextSize();     //計(jì)算文字占用尺寸
    calcFormSize();     //計(jì)算表格區(qū)域尺寸
    calcXTextList();    //計(jì)算X軸的文字內(nèi)容列表
    calcBaseLines();    //計(jì)算平行于X軸的輔助線
    calcData();         //計(jì)算數(shù)據(jù)對(duì)應(yīng)的位置
}

接下來(lái)就是計(jì)算這些數(shù)據(jù)的具體實(shí)現(xiàn),代碼的實(shí)現(xiàn)有很多種,以下的方法只是其中一種實(shí)現(xiàn)方式而已饿这。主要還是calc的邏輯

private void calcMaxYValue() {
    double max = 0;
    for (Integer color : mDatas.keySet()) {
        for (Units units : mDatas.get(color)) {
            max = Math.max(max, units.y);
        }
    }
    mMaxUsefulY = max;
}
private void calcMinYValue() {
    double min = 0;
    for (Integer color : mDatas.keySet()) {
        List<Units> list = mDatas.get(color);
        for (int i = 0; i < list.size(); i++) {
            if (i == 0) {
                min = list.get(i).y;
            }
            min = Math.min(min, list.get(i).y);
        }
    }
    mMinUsefulY = min;
}
private void calcYSpacing() {
    mUsefulY = mMaxUsefulY - mMinUsefulY;
    if (mUsefulY == 0) {
        mMaxUsefulY = mMinUsefulY + 80;
        mUsefulY = 80.0;
    }
    int minSpacing = (int) (mUsefulY / 6);
    if (minSpacing == 0) {
        int w = (mMaxUsefulY + "").length();
        int spacing = w / 10;
        if (spacing != 0) {
            mYDataSpacing = spacing;
        } else if (mMaxUsefulY == 0) {
            mYDataSpacing = 20;
        } else if (mMaxUsefulY <= 1) {
            mYDataSpacing = 1;
        } else {
            mYDataSpacing = 2;
        }
        return;
    }
    String s = minSpacing + "";
    int length = s.length() - 1 > 0 ? s.length() - 1 : 0;
    int unit = (int) (1 * Math.pow(10, length));
    for (int i = 1; i <= 10; i += 1) {
        if (mUsefulY / (i * unit) < 6) {
            mYDataSpacing = i * unit;
            return;
        }
    }
}
private void calcYTextList() {
    mYTexts = new ArrayList<>();
    if (mYDataSpacing == 1) {
        mMaxUsefulY = 1.0;
    }
    double remainder = mMaxUsefulY % mYDataSpacing;
    for (double i = mMaxUsefulY - remainder + mYDataSpacing; i >= mMinUsefulY - mYDataSpacing && i >= 0; i -= mYDataSpacing) {
        mYTexts.add((int) i + "");
    }
    String maxY = mYTexts.get(0);
    mMaxYValue = Double.parseDouble(maxY);
    String minY = mYTexts.get(mYTexts.size() - 1);
    mMinYValue = Double.parseDouble(minY);
}
private void calcTextSize() {
    String xMax = "";
    for (Integer integer : mDatas.keySet()) {
        List<Units> units = mDatas.get(integer);
        for (Units unit : units) {
            xMax = unit.x.length() > xMax.length() ? unit.x : xMax;
        }
    }
    mPaint.setTextSize(DisplayUtils.dp2px(getContext(), mBaseTextSize));
    Rect bounds = new Rect();
    mPaint.getTextBounds(xMax, 0, xMax.length(), bounds);
    mMaxXTextHeight = bounds.height();
    mMaxXTextWidth = bounds.width();
    mPaint.getTextBounds(mYTexts.get(0), 0, mYTexts.get(0).length(), bounds);
    mMaxYTextHeight = bounds.height();
    mMaxYTextWidth = bounds.width();
    mMaxXTextHeight = Math.max(mMaxXTextHeight, mMaxYTextHeight);
    mMaxYTextHeight = Math.max(mMaxXTextHeight, mMaxYTextHeight);
    mMaxYTextWidth = Math.max(mMaxYTextWidth, mMaxXTextWidth / 2 - mTextMarginX);
}
private void calcFormSize() {
    mFormWidth = getWidth() - mTextMarginX - mMaxYTextWidth - mMaxXTextWidth / 2 - 1;
    mFormHeight = getHeight() - mTextMarginY - mMaxXTextHeight - mMaxYTextHeight;
}
private void calcXTextList() {
    mXTexts.clear();
    mXSpacingCount = 1;
    Iterator<Integer> it = mDatas.keySet().iterator();
    if (it.hasNext()) {
        Integer next = it.next();
        List<Units> units = mDatas.get(next);
        while ((units.size() / mXSpacingCount + 1) * mMaxXTextWidth > mFormWidth * 2 / 3) {
            mXSpacingCount++;
        }
        for (int i = 0; i < units.size(); i++) {
            mXTexts.add(units.get(i).x + "");
        }
        return;
    }
}
private void calcBaseLines() {
    mLineSpacingCount = (mYTexts.size() - 1) / 2;
    if (mLineSpacingCount == 0) {
        mLineSpacingCount = 1;
    }
    mLineSpacingCountRemainer = (mYTexts.size() - 1) % mLineSpacingCount;
}
private void calcData() {
    Iterator<Integer> it = mDatas.keySet().iterator();
    int size = mXTexts.size();
    while (it.hasNext()) {
        List<Point> listPoint = new ArrayList<>();
        List<Rect> listRect = new ArrayList<>();
        Integer color = it.next();
        List<Units> units = mDatas.get(color);
        for (int i = 0; i < units.size(); i++) {
            float x = i * mFormWidth / (size - 1) + mMaxYTextWidth + mTextMarginY;
            float y = (float) ((mMaxYValue - units.get(i).y) * mFormHeight / (mMaxYValue - mMinYValue) + mMaxYTextHeight);
            listPoint.add(new Point((int) x, (int) y));
            listRect.add(new Rect((int) (x - mPointTouchWith), (int) (y - mPointTouchWith), (int) (x + mPointTouchWith), (int) (y + mPointTouchWith)));
        }
        mDataPoints.put(color, listPoint);
        mDataRects.put(color, listRect);
    }
}

以上,該計(jì)算的都計(jì)算了,onDraw方法此時(shí)已經(jīng)可以將我們的數(shù)據(jù)繪制出來(lái)了,但是產(chǎn)品的需求是在我們點(diǎn)擊touch的時(shí)候還需要繪制輔助線并顯示輔助文本,因此,我們還需要去重寫(xiě)onTouchEvent方法:

public boolean onTouchEvent(MotionEvent event) {
    int pointerCount = event.getPointerCount();
    if (pointerCount > 1) {
        getParent().requestDisallowInterceptTouchEvent(false);
        return false;
    }
    mXPosition = event.getX();
    mYPosition = event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mDownEventMills = Calendar.getInstance().getTimeInMillis();
            break;
        case MotionEvent.ACTION_MOVE:
            break;
        case MotionEvent.ACTION_UP:
            mUpEventMills = Calendar.getInstance().getTimeInMillis();
            break;
        default:
            break;
    }
    if (mUpEventMills - mDownEventMills < 100 && mUpEventMills > mDownEventMills) {
        mPreRect = null;
    }
    getParent().requestDisallowInterceptTouchEvent(true);
    invalidate();
    return true;
}

注意:因?yàn)槲覀兊膄ormView要添加touch事件,所以當(dāng)formView被用在可以滾動(dòng)的ViewGroup中時(shí)撞秋,我們touch時(shí)可能會(huì)消耗掉touchEvent长捧,如果單獨(dú)處理將滑動(dòng)事件透?jìng)髦罺iewGroup,可能并不是我們想要的效果吻贿。所以我們添加了event.getPointer判斷串结,當(dāng)多點(diǎn)觸控時(shí),我們不消耗滑動(dòng)事件。這樣就能平滑的操作formView了肌割。

最后卧蜓,就是我們的drawText(canvas);drawLines(canvas);drawData(canvas);drawHelpLine(canvas);drawHelpText(canvas);在這里,大家可以在gitHub中查看代碼把敞,我們只漸變效果及輔助文本是如何被繪制的弥奸。

private void drawData(Canvas canvas) {
    Iterator<Integer> it = mDataPoints.keySet().iterator();
    while (it.hasNext()) {
        Path path = new Path();
        Integer color = it.next();
        List list = mDataPoints.get(color);
        for (int i = 0; i < list.size(); i++) {
            Point o = (Point) list.get(i);
            if (i == 0) {
                path.moveTo(o.x, o.y);
            } else {
                path.lineTo(o.x, o.y);
            }
        }

        mPaint.setColor(color);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(path, mPaint);
        //繪制漸變效果
        if (mNeedDrawShader) {
            path.lineTo(((Point) list.get(list.size() - 1)).x, mFormHeight + mMaxYTextHeight);
            path.lineTo(mMaxYTextWidth + mTextMarginX, mFormHeight + mMaxYTextHeight);
            path.lineTo(((Point) list.get(0)).x, ((Point) list.get(0)).y);

            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(Color.WHITE);
            canvas.drawPath(path, mPaint);

            Shader shder = new LinearGradient(getWidth() / 2, 0, getWidth() / 2, getHeight()
                    , color & Color.parseColor("#44ffffff")
                    , color & Color.parseColor("#11ffffff"), Shader.TileMode.CLAMP);
            mPaint.setShader(shder);
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(color);
            canvas.drawPath(path, mPaint);
            mPaint.setShader(null);
        }
    }
}

因?yàn)檩o助文本是自定義的,所以我們需要對(duì)外提供一個(gè)借口奋早,用來(lái)設(shè)置輔助文本內(nèi)容盛霎。文本的model包含兩個(gè)屬性int color;String text;

public static class TextUnit {
    int    color;
    String text;
    public TextUnit(int color, String text) {
        this.color = color;
        this.text = text;
    }
}

最終使用List<List<TextUnit>> mDataTexts來(lái)保存需要展示的文本耽装。

private void drawHelpText(Canvas canvas) {
        if (!calcHelpTextSize()) {
            return;
        }
        Drawable drawable = ContextCompat.getDrawable(getContext(), mHelpTextBgResId);
        Rect rect = calcHelpRect();
        if (rect == null) {
            return;
        }
        drawable.setBounds(rect);
        drawable.draw(canvas);
        Rect bounds = new Rect();
        int margin = DisplayUtils.dp2px(getContext(), mHelpTextMargin);
        int height = (mMaxHelpTextHeight - margin * 2 - (mDataTexts.size() - 1) * DisplayUtils.dp2px(getContext(), 4)) / mDataTexts.size();
        mPaint.setTextSize(DisplayUtils.dp2px(getContext(), mHelpTextSize));
        for (int i = 0; i < mDataTexts.size(); i++) {
            int width = 0;
            for (TextUnit unit : mDataTexts.get(i)) {
                mPaint.setColor(unit.color);
                mPaint.getTextBounds(unit.text, 0, unit.text.length(), bounds);
                canvas.drawText(unit.text, rect.left + margin + width, rect.top + height + margin + i * (height + DisplayUtils.dp2px(getContext(), 4)), mPaint);
                width += bounds.width();
            }
        }
    }

最后愤炸,展示一下實(shí)際效果圖

效果圖
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市掉奄,隨后出現(xiàn)的幾起案子规个,更是在濱河造成了極大的恐慌,老刑警劉巖姓建,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绰姻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡引瀑,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)榨馁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)憨栽,“玉大人,你說(shuō)我怎么就攤上這事翼虫⌒既幔” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵珍剑,是天一觀的道長(zhǎng)掸宛。 經(jīng)常有香客問(wèn)我,道長(zhǎng)招拙,這世上最難降的妖魔是什么唧瘾? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮别凤,結(jié)果婚禮上饰序,老公的妹妹穿的比我還像新娘。我一直安慰自己规哪,他們只是感情好求豫,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般蝠嘉。 火紅的嫁衣襯著肌膚如雪最疆。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,950評(píng)論 1 291
  • 那天蚤告,我揣著相機(jī)與錄音努酸,去河邊找鬼。 笑死罩缴,一個(gè)胖子當(dāng)著我的面吹牛蚊逢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播箫章,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼烙荷,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了檬寂?” 一聲冷哼從身側(cè)響起终抽,我...
    開(kāi)封第一講書(shū)人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎桶至,沒(méi)想到半個(gè)月后昼伴,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镣屹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年圃郊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片女蜈。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡持舆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出伪窖,到底是詐尸還是另有隱情逸寓,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布覆山,位于F島的核電站竹伸,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏簇宽。R本人自食惡果不足惜勋篓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望魏割。 院中可真熱鬧生巡,春花似錦、人聲如沸见妒。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至盐股,卻和暖如春钱豁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背疯汁。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工牲尺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人幌蚊。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓谤碳,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親溢豆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蜒简,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容

  • 【Android 自定義View之繪圖】 基礎(chǔ)圖形的繪制 一、Paint與Canvas 繪圖需要兩個(gè)工具漩仙,筆和紙搓茬。...
    Rtia閱讀 11,653評(píng)論 5 34
  • 1、通過(guò)CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫(kù)組件 SD...
    陽(yáng)明先生_X自主閱讀 15,969評(píng)論 3 119