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();
    }

效果完成!欢迎大家关注交流。

时间: 2024-11-10 00:18:47

Android开发学习之路-自定义控件(天气趋势折线图)的相关文章

Android开发学习之路--网络编程之xml、json

一般网络数据通过http来get,post,那么其中的数据不可能杂乱无章,比如我要post一段数据,肯定是要有一定的格式,协议的.常用的就是xml和json了.在此先要搭建个简单的服务器吧,首先呢下载xampp,然后安装之类的就不再多讲了,参考http://cnbin.github.io/blog/2015/06/05/mac-an-zhuang-he-shi-yong-xampp/.安装好后,启动xampp,之后在浏览器输入localhost或者127.0.0.1就可以看到如下所示了: 这个就

android开发学习之路——连连看之游戏逻辑(五)

GameService组件则是整个游戏逻辑实现的核心,而且GameService是一个可以复用的业务逻辑类. (一)定义GameService组件接口 根据前面程序对GameService组件的依赖,程序需要GameService组件包含如下方法.   ·start():初始化游戏状态,开始游戏的方法.     ·Piece[][] getPieces():返回表示游戏状态的Piece[][]数组.     ·boolean hasPieces():判断Pieces[][]数组中是否还剩Piec

Android开发学习之路-RecyclerView滑动删除和拖动排序

Android开发学习之路-RecyclerView使用初探 Android开发学习之路-RecyclerView的Item自定义动画及DefaultItemAnimator源码分析 Android开发学习之路-下拉刷新怎么做? 本篇是接着上面三篇之后的一个对RecyclerView的介绍,这里多说两句,如果你还在使用ListView的话,可以放弃掉ListView了.RecyclerView自动帮我们缓存Item视图(ViewHolder),允许我们自定义各种动作的动画和分割线,允许我们对It

Android开发学习之路-该怎么学Android(Service和Activity通信为例)

在大部分地方,比如书本或者学校和培训机构,教学Android的方式都基本类似,就是告诉先上原理方法,然后对着代码讲一下. 但是,这往往不是一个很好的方法,为什么? ① 学生要掌握这个方法的用途,只能通过记忆而不是理解 ② 当某些原理稍微复杂的时候,通过讲解是不能直接理解的,有时候下课回去了再看也不一定看得明白 ③ 对英语文档不够重视,有问题先百度 本鸟自学Android一年,慢慢也学习到了很多的方法,如果你也是一个入门不久但是觉得很多东西都不明白的新手,希望本文对你有帮助. 我觉得要想学好And

Android开发学习之路--Broadcast Receiver初体验

学习了Activity组件后,这里再学习下另一个组件Broadcast Receiver组件.这里学习下自定义的Broadcast Receiver.通过按键自己发送广播,然后自己接收广播.新建MyBroadcastReceiver,代码如下: package com.example.jared.broadcasttest; import android.content.BroadcastReceiver; import android.content.Context; import andro

Android开发学习之路-环境搭建

这里选择使用android studio 集成开发环境,因为as是google推出的单独针对android开发的环境,并且迭代周期很快,因此,肯定会替代eclipse成为andorid的开发环境.对于没有eclipse基础的我来说,可以直接从as开始学习. 搭建环境, 1. 下载as withiout SDK 2. 导入自己的SDK库 3. 这里要求必须联网,而且,必须是可以FQ的,要不然速度会很慢. 4.SDK manager 如果速度比较慢,可以打开option勾选force http选项,

Android开发学习之路--UI之基本布局

上一篇文章中主要介绍了ui的控件,这里就学习下布局吧.android的基本布局在layout下主要如图: 从上图可以看出有FrameLayout(单帧布局),LinearLayout(线性布局),TableLayout(表格布局),RelativeLayout(相对布局),GridLayout(网格布局)等.具体的布局样式,在上图中也可以简单地看出来. 这里先介绍下android的View,ViewGroup,Layout. 1.View:代表了用户界面的一块可绘制区域.每个View在屏幕上占据

Android开发学习之路--图表实现(achartengine/MPAndroidChart)之初体验

??已经有一段时间没有更新博客了,在上周离开工作了4年的公司,从此不再安安稳稳地工作了.很多其它的是接受挑战和实现自身价值的提高. 离开了嵌入式linux,从此拥抱移动互联网,或许有点为时已晚.但是相信通过努力,什么时候都不会太晚.关于转行,关于这次的转型会不会成功,都是未知数,谁知道呢. 以后就好好学习互联网相关的知识.偶尔业余玩玩树莓派,玩玩机器人之类的. ??时间过得非常快,已经在新公司待了一周了,简单熟悉了环境.熟悉了产品,也学了些第三份框架的使用,什么data binding, ret

Android开发学习之路--Content Provider之初体验

天气说变就变,马上又变冷了,还好空气不错,阳光也不错,早起上班的车上的人也不多,公司来的同事和昨天一样一样的,可能明天会多一些吧,那就再来学习android吧.学了两个android的组件,这里学习下第三个android的组件,Content Provider内容提供器. Content Provider向我们提供了在不同应用程序之间的数据共享,比如微信啊,支付宝啊,想要获取手机联系人的信息,而手机联系人是另一个应用程序,那么这时候就需要用到Content Provider了.Content P