自定义列表,带你学习View

还有十几天就毕业啦,给想从事Android的同学说点心里话,如果你刚开始学Android,我建议你赶紧换方向,实在是太难找工作了(巨坑),安卓从2010年开始火到现在都7年了,技术优秀的大牛和普通技术的人都已经一大批啦,我好几个同学都转Java了(基础水平),我的Android技术实话都是参加工作实习后快速提高的(大学里当然是打游戏泡妞的啦!),你如果是要往Android那你就要会的比别人多,不要老用库写软件(没有技术含量),出来实习面试千万要谦虚,我当初就是觉得自己很牛逼跟面试官说相比。忙完了论文(向公司请了1个月的假,回去就要转正辣),自己写了撸了个图表demo,主要是知道回公司要做的APP有很多数据显示。进入正文:
做自定义view,需求是

1. 动态添加横向和竖向标题
2. 动态向单元格写入内容
3. 数据太多,需要滚动显示

在写自定义列表CellChatView前,我们先来认识onMeasure()的使用,我的理解:该方法是子view通知上一层父容器自己设置的宽高大小,需要再调用setMeasuredDimension(int w,int h)设置自定义view的宽高大小,该方法会触发父控件要求子控件的宽高遵循MeasureSpec的3种模式,1).at_mostly,随便设置。2).exactly,确定的大小如50dp.3). 基本不用不讲。

开始画图表,继承容器的选4大layout,搞控件的继承View就可以。
思路:画图表,横向我们可以每一行循环地画矩形,竖向我们画若干条竖线就可以。备注:控件最麻烦的就是宽高的算法,大家可以多看大神们的思路方法。

package cn.jason.com.chatdemo;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.RelativeLayout;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by jason on 17/5/21.
 */

public class CellChatView extends View {

    public static final String TAG = "CellChat>>";
    private Context context;

    public CellChatView(Context context) {
        super(context, null);
    }

    public CellChatView(Context context, AttributeSet set) {
        super(context, set);
        this.context = context;
        init();
    }

    int cellHeight;
    float fontSize = 18f;

    //画笔 文字
    private Paint textPaint;
    //画笔 文字
    private Paint dataPaint;
    //画笔 表格
    private Paint chatPaint;
    //横向 标题
    public List<String> xlist;
    //纵向 标题
    public List<String> ylist;
    //横向 小标题
    public List<String> fxlist;
    //数据
    public Map<Integer, List> dataMap = new HashMap<>();
    public boolean isAddData = false;

    private void init() {

        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        dataPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        chatPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        textPaint.setColor(Color.BLACK);
        dataPaint.setColor(Color.YELLOW);
        chatPaint.setColor(Color.BLACK);

        chatPaint.setStrokeWidth(2);

        textPaint.setTextSize(22f);                     // 文字大小
        textPaint.setTypeface(Typeface.SANS_SERIF);     // 字体类型
        textPaint.setTextAlign(Paint.Align.CENTER);     // 居中
        textPaint.setAntiAlias(true);                   // 是否抗锯齿

        xlist = new ArrayList<>();
        ylist = new ArrayList<>();
        fxlist = new ArrayList<>();

        //处理view的高度
        this.post(new Runnable() {
            @Override
            public void run() {
                int viewHeight = getHeight() / 2;                 //view从屏幕一半开始
                cellHeight = dpToPx(context, 30);
                int needHeight = cellHeight * (ylist.size()) * 2;
                Log.v(TAG, "viewHeight=" + viewHeight);
                Log.v(TAG, "needHeight=" + needHeight);
                if (viewHeight < needHeight) {              //当数据很多时,延长view
                    RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
                    params.height = needHeight + viewHeight+cellHeight;  //再加一个cellHeight是因为华为手机 的虚拟键会隐藏,影响高度
                    setLayoutParams(params);
                    Log.v(TAG, "viewHeight2=" + getHeight());
                } else if (viewHeight > needHeight) {
                    RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
                    params.height = viewHeight + needHeight;
                    setLayoutParams(params);
                    Log.v(TAG, "cellHeight=" + cellHeight);
                }

            }
        });
//        Log.v(TAG, xlist.size() + " " + ylist.size());
    }


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

        // 1.获取屏幕的宽高
        WindowManager wm = (WindowManager) context
                .getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(dm);
        int cwidth = dm.widthPixels;   //单位像素px
        int cheight = dm.heightPixels;  //注意 /2

        Log.v(TAG, "cwidth=" + cwidth + " cheight=" + cheight);

        //最小行的高度  px(不用px的在不同分辨率的手机界面会被遮挡的)
//        cellHeight = cheight  / ((ylist.size() + 1) * 2);
        //最大单元格的长度
        int cellWidth = cwidth / xlist.size();

        //绘制背景     px
        for (int i = 0; i < ylist.size(); i++) {
            addCell(canvas, cellWidth, cellHeight, i, cheight / 2);
        }

        //画表格线 竖线
        for (int i = 0; i < xlist.size(); i++) {
            if (i > 0) {
                canvas.drawLine(cellWidth * i, cheight / 2, cellWidth * i, cheight / 2 + cellHeight * (2 * ylist.size() - 1), chatPaint);
            }
        }
        for (int i = 1; i < xlist.size(); i++) {
            if (i < xlist.size() - 1) {
                canvas.drawLine(i * cellWidth + cellWidth / 2, cheight / 2 + cellHeight, i * cellWidth + cellWidth / 2, cheight / 2 + cellHeight * (2 * ylist.size() - 1), chatPaint);
            }
        }

        //画标题横线
        canvas.drawLine(0, cheight / 2, cwidth, cheight / 2, chatPaint);
        canvas.drawLine(cellWidth, cheight / 2 + 2 * cellHeight, getWidth() - cellWidth, cheight / 2 + 2 * cellHeight, chatPaint);
        //画底部横线
        canvas.drawLine(0, cheight / 2 + cellHeight * (2 * ylist.size() - 1), cwidth, cheight / 2 + cellHeight * (2 * ylist.size() - 1), chatPaint);

        //添加数据
        if (isAddData) {
            addServiceData(canvas, cellWidth, cellHeight, cheight / 2);
            isAddData = false;
            dataMap.clear();
        }
    }

    /**
     * @param canvas
     * @param cellWidth  最大单元格的宽度
     * @param cellHeight 最小单元格的高度
     * @param index      第几行
     * @param centerY    起始纵向坐标 ,这里需求是屏幕一半的位置
     */
    private void addCell(Canvas canvas, int cellWidth, int cellHeight, int index, int centerY) {
        Paint paintBK = new Paint(Paint.ANTI_ALIAS_FLAG);

        if (index == 0) {
            paintBK.setColor(Color.GREEN);
        } else if (index % 2 == 1) {
            paintBK.setColor(Color.WHITE);
        } else if (index % 2 == 0) {
            paintBK.setColor(0xfff2f2f2);
        }
        for (int i = 0; i < xlist.size(); i++) {
            Rect rect;
            if (index == 0) {
                rect = new Rect(cellWidth * i, centerY, cellWidth * i + cellWidth, centerY + cellHeight);
                canvas.drawRect(rect, paintBK);
                addDataText(canvas, xlist.get(i), fontSize-(xlist.size()-4)*2, rect);
            } else {
                rect = new Rect(cellWidth * i,
                        centerY + cellHeight + (index - 1) * 2 * cellHeight,
                        cellWidth * i + cellWidth,
                        centerY + cellHeight + (index - 1) * 2 * cellHeight + 2 * cellHeight);
                canvas.drawRect(rect, paintBK);
                if (i == 0) {
                    addDataText(canvas, ylist.get(index), fontSize-(xlist.size()-4)*2, rect);
                }
                if (index == 1) {
                    if (i > 0 && i < xlist.size() - 1) {            //第一行 中间的小标题
                        if (fxlist.size() - 1 < 2 * (i - 1)) {      //处理小标题没有了
                            return;
                        }
                        Rect titleRect1 = new Rect(cellWidth * i, centerY + cellHeight, cellWidth * i + cellWidth / 2, centerY + cellHeight * 2);
                        Rect titleRect2 = new Rect(cellWidth * i + cellWidth / 2, centerY + cellHeight, cellWidth * (i + 1), centerY + cellHeight * 2);
                        addDataText(canvas, fxlist.get((i - 1) * 2), fontSize -6-(xlist.size()-4)*3, titleRect1);
                        addDataText(canvas, fxlist.get((i - 1) * 2 + 1), fontSize - 6-(xlist.size()-4)*3, titleRect2);
                    }
                }
            }
//            canvas.drawRect(rect, paintBK); //注意画 背景 画 字体 的执行顺序(覆盖)
        }
    }

    private void addServiceData(Canvas canvas, int cellWidth, int cellHeight, int centerY) {
        String text1 = null;
        String text2 = null;
        for (int i = 1; i < ylist.size(); i++) {
            for (int j = 1; j < xlist.size(); j++) {

                if (i == 1) {
                    //第一行数据
                    if (j > 0 && xlist.size() - 1 > j) {

                        Rect rect1 = new Rect(cellWidth * j, centerY + 2 * cellHeight, cellWidth * j + cellWidth / 2, centerY + 3 * cellHeight);
                        Rect rect2 = new Rect(cellWidth * j + cellWidth / 2, centerY + 2 * cellHeight, cellWidth * (j + 1), centerY + 3 * cellHeight);
                        text1 = (String) dataMap.get(1).get(2 * (j - 1));
                        text2 = (String) dataMap.get(1).get(2 * (j - 1) + 1);

                        addDataText(canvas, text1, fontSize - 5-(xlist.size()-4)*2, rect1);
                        addDataText(canvas, text2, fontSize - 5-(xlist.size()-4)*2, rect2);
                    } else {
                        Rect rectPercent = new Rect(cellWidth * j, centerY + cellHeight, cellWidth * (j + 1), centerY + 3 * cellHeight);
                        text1 = (String) dataMap.get(1).get(2 * (j - 1));
                        addDataText(canvas, text1, fontSize + 1-(xlist.size()-4)*2, rectPercent);
                    }
                } else {
                    //第2.。行数据
                    if (dataMap.get(i) == null) {   //处理空白行数据
                        continue;
                    }
                    if (j > 0 && xlist.size() - 1 > j) {

                        Rect rect1 = new Rect(cellWidth * j, centerY + cellHeight + 2 * cellHeight * (i - 1), cellWidth * j + cellWidth / 2, centerY + cellHeight + 2 * cellHeight * (i - 1) + 2 * cellHeight);
                        Rect rect2 = new Rect(cellWidth * j + cellWidth / 2, centerY + cellHeight + 2 * cellHeight * (i - 1), cellWidth * (j + 1), centerY + cellHeight + 2 * cellHeight * (i - 1) + 2 * cellHeight);

                        text1 = (String) dataMap.get(i).get(2 * (j - 1));
                        text2 = (String) dataMap.get(i).get(2 * (j - 1) + 1);

                        addDataText(canvas, text1, fontSize - 5-(xlist.size()-4)*2, rect1);
                        addDataText(canvas, text2, fontSize - 5-(xlist.size()-4)*2, rect2);
                    } else {
                        Rect rectPercent = new Rect(cellWidth * j, centerY + cellHeight + 2 * cellHeight * (i - 1), cellWidth * (j + 1), centerY + cellHeight + 2 * cellHeight * (i));
                        text1 = (String) dataMap.get(i).get(2 * (j - 1));
                        addDataText(canvas, text1, fontSize + 1-(xlist.size()-4)*2, rectPercent);
                    }
                }
            }
        }

    }

    /**
     * @param canvas
     * @param text     绘制的文本
     * @param fontSize 文本大小
     * @param targetRect 绘制的矩形区域,这里是居中显示
     */
    private void addDataText(Canvas canvas, String text, float fontSize, Rect targetRect) {

       /* Rect rectText = new Rect();
        textPaint.getTextBounds(text.toString(), 0, text.toString().length(), rectText); // 获取字符串的宽高

        canvas.save();
        canvas.clipRect(rect); // 屏幕裁剪区域
        if (indexY == 0) {
            canvas.drawText(text, indexX * cellWidth + cellWidth / 2, centerY + (cellHeight + rectText.height()) / 2, textPaint);
        } else {
            canvas.drawText(text, indexX * cellWidth + cellWidth / 2, centerY + cellHeight * (indexY) + (indexY - 1) * 2 * cellHeight + (2*cellHeight + rectText.height()) / 2, textPaint);
        }
        canvas.restore();*/
        textPaint.setTextSize(dpToPx(context, fontSize));
        textPaint.setColor(Color.BLACK);
        Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
        int baseline = targetRect.top + (targetRect.bottom - targetRect.top) / 2 - (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.top;

        // 下面这行是实现水平居中,drawText对应改为传入targetRect.centerX()
        textPaint.setTextAlign(Paint.Align.CENTER);
        canvas.drawText(text, targetRect.centerX(), baseline, textPaint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(measureHeight(widthMeasureSpec), measureHeight(heightMeasureSpec));
    }

    private int measureHeight(int heightMeasureSpec) {
        int result = 0;

        int specMode = MeasureSpec.getMode(heightMeasureSpec);
        int specSize = MeasureSpec.getSize(heightMeasureSpec);
        switch (specMode) {
            case MeasureSpec.AT_MOST:       //父wrap
//                Log.e(TAG, "子容器可以是声明大小内的任意大小");
//                Log.e(TAG, "大小为:" + specSize);
                result = specSize;
                break;
            case MeasureSpec.EXACTLY:       //子40dp=80px
//                Log.e(TAG, "父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间");
//                Log.e(TAG, "大小为:" + specSize);
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:   //父容器wrap ,子1999dp
//                Log.e(TAG, "父容器对于子容器没有任何限制,子容器想要多大就多大");
//                Log.e(TAG, "大小为:" + specSize);
                result = 1500;
                break;
            default:
                break;
        }
        return result;
    }

//    private void testText(Canvas canvas) {
//        Rect targetRect = new Rect(50, 50, 700, 200);
//        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
//        paint.setStrokeWidth(3);
//        paint.setTextSize(30);
//        String testString = "测试:ijkJQKA:1234";
//        paint.setColor(Color.CYAN);
//        canvas.drawRect(targetRect, paint);
//        paint.setColor(Color.RED);
//        Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();  //一定要在paint参数设置后再调用
//
//        //fon.top的参考值是baseline,它在baseline的上方,固为负值
//        int baseline = targetRect.top + (targetRect.bottom - targetRect.top) / 2 - (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.top;
//        // 下面这行是实现水平居中,drawText对应改为传入targetRect.centerX()
//        paint.setTextAlign(Paint.Align.CENTER);
//        canvas.drawText(testString, targetRect.centerX(), baseline, paint);
//    }

//    int lastX = 0;
//    int lastY = 0;
//    @Override
//    public boolean onTouchEvent(MotionEvent event) { //使view可以跟随手指滑动
//
//        //获取到手指处的横坐标和纵坐标
//        int x = (int) event.getX();
//        int y = (int) event.getY();
//
//
//        switch (event.getAction()) {
//            case MotionEvent.ACTION_DOWN:
//
//                lastX = x;
//                lastY = y;
//
//                break;
//
//            case MotionEvent.ACTION_MOVE:
//
//                //计算移动的距离
//                int offX = x - lastX;
//                int offY = y - lastY;
//                //调用layout方法来重新放置它的位置
//                layout(getLeft() + offX, getTop() + offY,
//                        getRight() + offX, getBottom() + offY);
//
//                break;
//        }
//
//        return true;
//    }


    /**
     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
     */
    public static int dpToPx(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
}

//布局
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_cell_chat"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true"
    android:overScrollMode="never"
    android:scrollbars="none"
    tools:context="cn.jason.com.chatdemo.CellChatActivity">


    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_centerInParent="true"
            android:onClick="click_ADD"
            android:text="ADD DATA" />

        <cn.jason.com.chatdemo.CellChatView
            android:id="@+id/cellChat"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </RelativeLayout>


</ScrollView>

//在activity中
package cn.jason.com.chatdemo;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

import java.util.ArrayList;

public class CellChatActivity extends AppCompatActivity {

    private CellChatView cellChat;

    private String TAG = "CellChat>>";


    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 11) {

                cellChat.postInvalidate();
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cell_chat);

        ActionBar actionBar = getSupportActionBar();
        actionBar.setTitle("添加数据");
        actionBar.hide();

        cellChat= (CellChatView) findViewById(R.id.cellChat);

        cellChat.xlist.add("科目");   //横向标题
        cellChat.xlist.add("应上课时");
        cellChat.xlist.add("实上课时");
        cellChat.xlist.add("上课率");
//        cellChat.xlist.add("上课率");

        cellChat.ylist.add("科目");   //竖向标题
        cellChat.ylist.add("钢琴");
        cellChat.ylist.add("拉丁");
        cellChat.ylist.add("合计");
        cellChat.ylist.add("kk");
        cellChat.ylist.add("kk");
        cellChat.ylist.add("kk");
        cellChat.ylist.add("kk");
        cellChat.ylist.add("kk");
        cellChat.ylist.add("kk");
        cellChat.ylist.add("kk");
        cellChat.ylist.add("kk");

        cellChat.fxlist.add("个体课");  //第二行的小标题,index=1的行
        cellChat.fxlist.add("集体课");
        cellChat.fxlist.add("个体课");
        cellChat.fxlist.add("集体课");


//        mHandler.sendEmptyMessage(11);

        //测试 内存泄漏 的列子,没什么用
//        ActivityManager.getInstance().addActivity(this);

    }

    int i = 0;

    public void click_ADD(View view) {
        cellChat.isAddData = true;
        i += 1;
        //用单个list添加完数据clear(),发现每一行都用同一个list,clear后新加入的数据无效,暂时用多个list
        ArrayList<String> list1 = new ArrayList<>();
        ArrayList<String> list2 = new ArrayList<>();
        ArrayList<String> list3 = new ArrayList<>();
        ArrayList<String> list4 = new ArrayList<>();

        list1.add(String.valueOf(i + 10));
        list1.add("10");
        list1.add("10");
        list1.add("10");
        list1.add("19%");

        cellChat.dataMap.put(1, list1);

        list2.add("20");
        list2.add("10");
        list2.add("10");
        list2.add("10");
        list2.add("29%");
//        cellChat.dataMap.put(2, list2);

        list3.add("30");
        list3.add("10");
        list3.add("10");
        list3.add("10");
        list3.add("39%");
        cellChat.dataMap.put(3, list3);

//        list4.add("40");
//        list4.add("10");
//        list4.add("10");
//        list4.add("10");
//        list4.add("49%");
//        cellChat.dataMap.put(4, list4);

        cellChat.postInvalidate();
    }
}
这里我说一下,原本做本图表时只要4列4行固定的,不能有空白数据的单元格,在屏幕一半的位置开始。后来想多适配些场景,代码就没有简化,方法的调用参数填充都是按计算方式来,没有简单化成简式,所以有些地方比较长。如今图表可以多行多列,可滑动,能够简单处理空白标题和空白数据。

本图表还为大家添加了一个 内存泄漏的库使用
使用内存泄漏检测器

1.MyApplication的increase中 …if (LeakCanary.isInAnalyzerProcess(this)) {
 // This process is dedicated to LeakCanary for heap analysis.
 // You should not init your app in this process.
 return;
}
LeakCanary.install(this);

2.gradle …debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.5.1’
releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.5.1’
testCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.5.1’

不足:图表的高度我并没有细心的用准确的算法,所以最后会有白的地方,头部的actionbar高度和padding等都没算;填充数据的方式好笨,每填充一行内容就要new一个ArrayList,在做的时候就算保存到view的数据集datamap后再clear再加入新的数据,汇报空指针,这里应该是内存地址的引用直接将保存的数据也clear()了,希望大家可以说出好的数据填充方式。

亮点:图表可以2级标题,多行多列可滑动。

注意事项:开发中720p的手机可以直接设置dp,但到了1080p就不可以,整个view会变形。大家需要将用到的dp值全部转为dx(分辨率),dp*2=dp;文字居中在文中大家以后可以调用我的方法就可,这是最完美最准确的方式。

初次写稿,难免有考虑不周的地方,希望大家共同进步,指导意见。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值