Android開發學習之路-自定義控件(天氣趨勢折線圖)


之前寫了個天氣APP,帶4天預報和5天歷史信息。所以想着要不要加一個折線圖來顯示一下天氣變化趨勢,難得有空,就寫了一下,這里做些記錄,腦袋不好使容易忘事。

先放一下效果:

控件內容比較簡單,就是一個普通的折線圖,上下分別帶有數字,點擊的時候顯示當天溫度的差值。


 

創建一個類繼承自View,並添加兩個構造方法:

public class TrendGraph extends View {
    public TrendGraph(Context context) { // 在java代碼中創建調用
        super(context);
    }

    public TrendGraph(Context context, AttributeSet attrs) { // 在xml中創建調用
        super(context, attrs);
    }
}

 

 

因為這里不需要考慮wrap_content的情況,所以onMeasure方法不需重寫,關鍵的是onDraw,而onDraw方法其實也不困難,只需要確定好每個點的具體位置就好,因為連線也是需要點的坐標,代碼比較啰嗦,可以略過:

 1     @Override
 2     protected void onDraw(Canvas canvas) {
 3         super.onDraw(canvas);
 4         if (mElements == null || mElements.size() == 0) {
 5             return;
 6         }
 7         double max_up = getMaxUp();
 8         double min_down = getMinDown();
 9         canvas.setDrawFilter(mDrawFilter);
10         mPaint.setStrokeWidth(lineWeith);
11         float width = getWidth();
12         float grap = width / mElements.size();
13         float textSize = mTextPaint.getTextSize();
14         int textMargin = circleRadius * 2;
15         float margin_top = textSize + 2 * textMargin;
16         Log.d(TAG, "onDraw: " + margin_top + "|" + textSize);
17         float height = getHeight() - 2 * margin_top;
18 
19         for (int i = 0; i < mElements.size() - 1; i++) {
20             float startX = i * grap + grap / 2;
21             float stopX = (i + 1) * grap + grap / 2;
22             float startY = (float) (max_up - mElements.get(i).getUp()) / (float) (max_up -
23                     min_down) * height + margin_top;
24             float stopY = (float) (max_up - mElements.get(i + 1).getUp()) / (float) (max_up -
25                     min_down) * height + margin_top;
26 
27             canvas.drawText((int) mElements.get(i).getUp() + "℃", startX - textSize, startY -
28                     textMargin, mTextPaint);
29             canvas.drawCircle(startX, startY, circleRadius, mPaint);
30             canvas.drawLine(startX, startY, stopX, stopY, mPaint);
31             if (i == mElements.size() - 2) {
32                 canvas.drawText((int) mElements.get(i + 1).getUp() + "℃", stopX - textSize, stopY
33                         - textMargin, mTextPaint);
34                 canvas.drawCircle(stopX, stopY, circleRadius, mPaint);
35             }
36 
37             startY = (float) (max_up - mElements.get(i).getDown()) / (float) (max_up - min_down) *
38                     height + margin_top;
39             stopY = (float) (max_up - mElements.get(i + 1).getDown()) / (float) (max_up -
40                     min_down) * height + margin_top;
41             canvas.drawText((int) mElements.get(i).getDown() + "℃", startX - textSize, startY +
42                     textSize + textMargin, mTextPaint);
43             canvas.drawCircle(startX, startY, circleRadius, mPaint);
44             canvas.drawLine(startX, startY, stopX, stopY, mPaint);
45             if (i == mElements.size() - 2) {
46                 canvas.drawText((int) mElements.get(i + 1).getDown() + "℃", stopX - textSize,
47                         stopY + textSize + textMargin, mTextPaint);
48                 canvas.drawCircle(stopX, stopY, circleRadius, mPaint);
49             }
50         }
51     }

 

 

考慮到需要允許用戶進行簡單的設置,例如點的大小,文字大小等等,所以定義一些自定義屬性(res/values/attr.xml):

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TrendGraph">
        <attr name="lineWidth" format="dimension"/>
        <attr name="circleRadius" format="dimension" />
        <attr name="textSize" format="dimension" />
        <attr name="textColor" format="reference" />
    </declare-styleable>
</resources>

 

 format指該屬性的格式,指定為dimension則是尺寸,取值單位是dp、sp或px等等,而reference則是引用,即一般在xml中引用其他資源的寫法,如@string/app_name。還有其他類型,可以自行查找文檔。

 

對自定義屬性進行解析得到,這個解析需要在上面定義的第二個構造方法中進行,代碼如下:

    public TrendGraph(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.TrendGraph);
        circleRadius = array.getDimensionPixelSize(R.styleable.TrendGraph_circleRadius, 5);
        lineWeith = array.getDimensionPixelSize(R.styleable.TrendGraph_lineWidth, 3);
        mTextPaint.setTextSize(array.getDimensionPixelSize(R.styleable.TrendGraph_textSize, 35));
        mTextPaint.setColor(array.getColor(R.styleable.TrendGraph_textColor, Color.BLACK));
        array.recycle();
    }

getDimensionPixelSize方法則是通過傳入的值,轉換為具體的像素(px)值,也就免去我們手動轉換的麻煩。但是要注意,其中的defaultValue依然是px。

 

接着,就可以通過xml指定這些屬性,在布局中加入命名空間:

xmlns:app="http://schemas.android.com/apk/res-auto"

 

則Android Studio會自動引入,並且可以補全得到,具體使用:

    <com.fndroid.byweather.views.TrendGraph
        android:id="@+id/tg"
        android:layout_width="match_parent"
        app:textColor="@color/colorAccent"
        app:textSize="22sp"
        app:circleRadius="2dp"
        android:layout_height="200dp"/>

 


 

 

最后,添加一個事件監聽,在點擊View的時候進行回調:

① 定義接口:

    public interface onItemClickListener{
        void onItemClick(View view, Element element);
    }

 

② 在View中添加接口對象,並設置setter方法:

public class TrendGraph extends View {

    private onItemClickListener mOnItemClickListener;

    // 省略代碼

    public void setOnItemClickListener(onItemClickListener onItemClickListener) {
        mOnItemClickListener = onItemClickListener;
    }

}

 

③ 處理onTouchEvent,重寫該方法,代碼如下:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int viewWidth = getWidth();
        int itemWidth = viewWidth / mElements.size();
        int viewHeight = getHeight();
        boolean isMove = false; // 界面中最外層為一個NestedScrollView,所以為了避免滑動時也觸發,加入變量處理

        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                isMove = true;
                break;
            case MotionEvent.ACTION_UP:
                if (!isMove){ // 判斷只有點擊時進行回調
                    int position = (int) (event.getX() / itemWidth); // 取得點擊的位置
                    mOnItemClickListener.onItemClick(this, mElements.get(position)); // 回調
                }
                break;
        }

        return true;
    }

 

④ 在Activity中,進行監聽設置,並處理:

  historyGraph.setOnItemClickListener(this);
    @Override
    public void onItemClick(View view, TrendGraph.Element element) {
        int dt = (int) (element.getUp() - element.getDown());
        Snackbar.make(root, "當天溫差為:" + dt + "℃", Snackbar.LENGTH_SHORT).show();
    }

 

 

效果完成!歡迎大家關注交流。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM