Charts

2018-11-05  本文已影响38人  ngugg

This chapter covers the basic setup for using this library.

1. Getting Started

Add dependency

As a first step, add a dependency to this library to your project. How to do that is described in the usage section of this repository. Gradle is the recommended way of using this library as a dependency.

Creating the View

For using a LineChart, BarChart, ScatterChart, CandleStickChart, PieChart, BubbleChart or RadarChart, define it in .xml:

And then retrieve it from your Activity, Fragment or whatever:

  // in this example, a LineChart is initialized from xml
    LineChart chart = (LineChart) findViewById(R.id.chart);

or create it in code (and then add it to a layout):

    // programmatically create a LineChart
    LineChart chart = new LineChart(Context);

    // get a layout defined in xml
    RelativeLayout rl = (RelativeLayout) findViewById(R.id.relativeLayout);
    rl.add(chart); // add the programmatically created chart

Adding data

After you have an instance of your chart, you can create data and add it to the chart. This example uses the LineChart, for which the Entry class represents a single entry in the chart with x- and y-coordinate. Other chart types, such as BarChart use other classes (e.g. BarEntry) for that purpose.

To add data to your chart, wrap each data object you have into an Entry object, like below:

YourData[] dataObjects = ...;

List<Entry> entries = new ArrayList<Entry>();

for (YourData data : dataObjects) {

    // turn your data into Entry objects
    entries.add(new Entry(data.getValueX(), data.getValueY())); 
}

As a next step, you need to add the List<Entry> you created to a LineDataSet object. DataSet objects hold data which belongs together, and allow individual styling of that data. The below used "Label" has only a descriptive purpose and shows up in the Legend, if enabled.

LineDataSet dataSet = new LineDataSet(entries, "Label"); // add entries to dataset
dataSet.setColor(...);
dataSet.setValueTextColor(...); // styling, ...

As a last step, you need to add the LineDataSet object (or objects) you created to a LineData object. This object holds all data that is represented by a Chart instance and allows further styling. After creating the data object, you can set it to the chart and refresh it:

LineData lineData = new LineData(dataSet);
chart.setData(lineData);
chart.invalidate(); // refresh

Please consider the scenario above a very basic setup. For a more detailed explanation please refer to the setting data section, which explains how to add data to various Chart types based on examples.

Styling

For information about settings & styling of the chart surface and data, visit the general settings & styling section. Regarding more specific styling & settings for individual chart types, please have a look at the specific settings & styling wiki page.

2. Interaction with the Chart

This library allows you to fully customize the possible touch (and gesture) interaction with the chart-view and react to the interaction via callback-methods.

Enabling / disabling interaction

Chart fling / deceleration

Highlighting Values

How to allow highlighting entries via tap-gesture and programmatically is described int the highlightning section.

Gesture callbacks

The OnChartGestureListener will allow you to react to gestures made on the chart:

public interface OnChartGestureListener {

    /**
     * Callbacks when a touch-gesture has started on the chart (ACTION_DOWN) 
     * 在图表上启动触摸手势时回调(ACTION_DOWN)
     * @param me
     * @param lastPerformedGesture
     */
    void onChartGestureStart(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture);

    /**
     * Callbacks when a touch-gesture has ended on the chart (ACTION_UP, ACTION_CANCEL)
     * 触摸手势在图表上结束时的回调(ACTION_UP,ACTION_CANCEL)
     * @param me
     * @param lastPerformedGesture
     */
    void onChartGestureEnd(MotionEvent me, ChartTouchListener.ChartGesture lastPerformedGesture);

    /**
     * Callbacks when the chart is longpressed.
     *  图表被长按时回调。
     * @param me
     */
    public void onChartLongPressed(MotionEvent me);

    /**
     * Callbacks when the chart is double-tapped.
     *  双击图表时的回调。
     * @param me
     */
    public void onChartDoubleTapped(MotionEvent me);
/**
     * Callbacks when the chart is single-tapped.
     *  单击图表时的回调。
     * @param me
     */
    public void onChartSingleTapped(MotionEvent me);

    /**
     * Callbacks then a fling gesture is made on the chart.
     *  在图表上做出一个惊人的手势。
     * @param me1
     * @param me2
     * @param velocityX
     * @param velocityY
     */
    public void onChartFling(MotionEvent me1, MotionEvent me2, float velocityX, float velocityY);

   /**
     * Callbacks when the chart is scaled / zoomed via pinch zoom gesture.
     *  当图表通过缩放缩放手势缩放/缩放时。
     * @param me
     * @param scaleX scalefactor on the x-axis
     * @param scaleY scalefactor on the y-axis
     */
    public void onChartScale(MotionEvent me, float scaleX, float scaleY);

   /**
    * Callbacks when the chart is moved / translated via drag gesture.
    * 通过拖动手势移动/翻译图表时的回调。
    * @param me
    * @param dX translation distance on the x-axis  x轴上的平移距离
    * @param dY translation distance on the y-axis  y轴上的平移距离
    */
    public void onChartTranslate(MotionEvent me, float dX, float dY);
}

Simply let your class that should receive the callbacks implement this interface and set it as a listener to the chart:

chart.setOnChartGestureListener(this);

3. Highlighting Values

This section focuses on the topic of highlighting entries in the chart, both via tap-gesture and programmatically based on release v3.0.0.

Enabling / Disabling highlighting

In addition to that, highlighting can be configured for individual DataSet objects:

 dataSet.setHighlightEnabled(true); // allow highlighting for DataSet

  // set this to false to disable the drawing of highlight indicator (lines)
  dataSet.setDrawHighlightIndicators(true); 
  dataSet.setHighlightColor(Color.BLACK); // color for highlight indicator
  // and more...

Highlighting programmatically

highlightValue(float x, int dataSetIndex, boolean callListener): Highlights the value at the given x-position in the given DataSet. Provide -1 as the dataSetIndex to undo all highlighting. The boolean flag determines wether the selection listener should be called or not.

highlightValue(Highlight high, boolean callListener): Highlights the value represented by the provided Highlight object. Provide null to undo all highlighting. The boolean flag determines wether the selection listener should be called or not.

highlightValues(Highlight[] highs): Highlights the values represented by the given Highlight[] array. Provide null or an empty array to undo all highlighting.

getHighlighted(): Returns an Highlight[] array that contains information about all highlighted entries, their x-index and dataset-index.

Selection callbacks

This library provides a number of listeners for callbacks upon interaction. One of them is the OnChartValueSelectedListener, for callbacks when highlighting values via touch:

public interface OnChartValueSelectedListener {
    /**
    * Called when a value has been selected inside the chart.
    * 在图表中选择值时调用。
    * @param e The selected Entry.
    * @param h The corresponding highlight object that contains information
    * about the highlighted position
    * 相应的突出显示对象,包含有关突出显示位置的信息
    */
    public void onValueSelected(Entry e, Highlight h);
    /**
    * Called when nothing has been selected or an "un-select" has been made. 
    * 在未选择任何内容或已取消“取消选择”时调用。
    */
    public void onNothingSelected();
}

Simply let your class that should receive the callbacks implement this interface and set it as a listener to the chart:

chart.setOnChartValueSelectedListener(this);

The Highlight class

The Highlight class represents all data associated with a highlighted Entry, such as the highlighted Entry object itself, the DataSet it belongs to, it's position on the drawing surface and more. It can be used to get information about an already highlighted Entry, or used to provide information to the Chart for an Entry to be highlighted. Regarding that purpose, the Highlight class provides two constructors:

/** constructor for standard highlight */
// 标准高亮的构造函数
 public Highlight(float x, int dataSetIndex) { ... }

 /** constructor for stacked BarEntry highlight */
// 堆叠BarEntry高亮显示的构造函数
 public Highlight(float x, int dataSetIndex, int stackIndex) { ... }

These constructors can be used to create a Highlight object which allows to perform highlighting programmatically:

// highlight the entry and x-position 50 in the first (0) DataSet
// 突出显示第一个(0)数据集中的条目和x位置50
Highlight highlight = new Highlight(50f, 0); 

chart.highlightValue(highlight, false); // highlight this value, don't call listener
// 突出显示此值,不要调用侦听器

Custom highlighter

All user input in the form of highlight gestures is internally processed by the default ChartHighlighter class. It is possible to replace the default highligher with a custom implementation using the below method:

setHighlighter(ChartHighlighter highlighter): Sets a custom highligher object for the chart that handles / processes all highlight touch events performed on the chart-view. Your custom highlighter object needs to extend the ChartHighlighter class.

4. The Axis (AxisBase)

This wiki page focuses on the AxisBase class, the baseclass of both XAxis (XAxis) and YAxis(YAxis). Introduced in v2.0.0

The following methods mentioned below can be applied to both axes.

The axis classes allow specific styling and consist (can consist) of the following components/parts:
- 轴类允许特定样式并包含(可以包含)以下组件/部件:

Control which parts (of the axis) should be drawn

Customizing the axis range (min / max)

Styling / modifying the axis

Formatting axis values

For formatting axis values, you can use the IAxisValueFormatter interface, which is explained here. You can use the axis.setValueFormatter(IAxisValueFormatter formatter) method to set your custom formatter to the axis.

Limit Lines

Both axes support so called LimitLines that allow to present special information, like borders or constraints. LimitLines added to the YAxis are drawn in horizontal direction, and in vertical direction when added to the XAxis. This is how you add and remove LimitLines from the axis:

Limit lines (class LimitLine) are (as the name might indicate) plain and simple lines can be used to provide additional information for the user.
- 限制行(类LimitLine)是(如名称所示)简单行和简单行可用于为用户提供附加信息。

As an example, your chart might display various blood pressure measurement results the user logged with an application. In order to inform the user that a systolic blood pressure of over 140 mmHg is considered to be a health risk, you could add a LimitLine at 140 to provide that information.
- 例如,您的图表可能会显示用户使用应用程序记录的各种血压测量结果。 为了告知用户收缩压超过140 mmHg被认为是健康风险,您可以在140处添加LimitLine以提供该信息。

Example Code

YAxis leftAxis = chart.getAxisLeft();

LimitLine ll = new LimitLine(140f, "Critical Blood Pressure");
ll.setLineColor(Color.RED);
ll.setLineWidth(4f);
ll.setTextColor(Color.BLACK);
ll.setTextSize(12f);
// .. and more styling options

leftAxis.addLimitLine(ll);

5. XAxis

The XAxis is a subclass of AxisBase from which it inherits a number of styling and convenience methods.

The XAxis class (in versions prior to 2.0.0 XLabels called), is the data and information container for everything related to the the horizontal axis. Each Line-, Bar-, Scatter-, CandleStick- and RadarChart has an XAxis object.

The XAxis class allows specific styling and consists (can consist) of the following components/parts:

In order to acquire an instance of the XAxis class, do the following:
- 要获取XAxis类的实例,请执行以下操作:

XAxis xAxis = chart.getXAxis();

Customizing the axis values

Example Code

XAxis xAxis = chart.getXAxis();
xAxis.setPosition(XAxisPosition.BOTTOM);
xAxis.setTextSize(10f);
xAxis.setTextColor(Color.RED);
xAxis.setDrawAxisLine(true);
xAxis.setDrawGridLines(false);
// set a custom value formatter
xAxis.setValueFormatter(new MyCustomFormatter()); 
// and more...

6. YAxis

The YAxis is a subclass of AxisBase. This wiki entry only describes the YAxis, not it's superclass.

The YAxis class (in versions older than 2.0.0 YLabels called), is the data and information container for everything related with the vertical-axis. Each Line-, Bar-, Scatter or CandleStickChart has a left and a right YAxis object, responsible for either the left, or the right axis respectively. The RadarChart has only one YAxis. By default, both axes of the chart are enabled and will be drawn.

In order to acquire an instance of the YAxis class, call one of the following methods:

YAxis leftAxis = chart.getAxisLeft();
YAxis rightAxis = chart.getAxisRight();

YAxis leftAxis = chart.getAxis(AxisDependency.LEFT);

YAxis yAxis = radarChart.getYAxis(); // this method radarchart only

At runtime, use public AxisDependency getAxisDependency() to determine the side of the chart this axis represents.

Customizations that affect the value range of the axis need to be applied before setting data for the chart.

Axis Dependency

Per default, all data that is added to the chart plots against the left YAxis of the chart. If not further specified and enabled, the right YAxis is adjusted to represent the same scale as the left axis.

If your chart needs to support different axis scales, you can achieve that by setting the axis your data should plot against. This can be done by changing the AxisDependency of your DataSet object:

LineDataSet dataSet = ...; // get a dataset
dataSet.setAxisDependency(AxisDependency.RIGHT);

Setting this will change the axis your data is plotted against.

The zero line

Besides the grid-lines, that originate horizontally alongside each value on the YAxis, there is the so called zeroline, which is drawn at the zero (0) value on the axis, and is similar to the grid-lines but can be configured separately.

Zero-line example code:

// data has AxisDependency.LEFT
YAxis left = mChart.getAxisLeft();
left.setDrawLabels(false); // no axis labels
left.setDrawAxisLine(false); // no axis line
left.setDrawGridLines(false); // no grid lines
left.setDrawZeroLine(true); // draw a zero line
mChart.getAxisRight().setEnabled(false); // no right axis

The above code will result in a zero-line like shown in the image below. No axis values are drawn, no grid lines or axis lines are drawn, just a zero line.

image.png

More example code

YAxis yAxis = mChart.getAxisLeft();
yAxis.setTypeface(...); // set a different font
yAxis.setTextSize(12f); // set the text size
yAxis.setAxisMinimum(0f); // start at zero
yAxis.setAxisMaximum(100f); // the axis maximum is 100
yAxis.setTextColor(Color.BLACK);
yAxis.setValueFormatter(new MyValueFormatter());
yAxis.setGranularity(1f); // interval 1
yAxis.setLabelCount(6, true); // force 6 labels
//... and more

7. Setting Data

This chapter covers the topic of setting data to various kinds of Charts.

LineChart

If you want to add values (data) to the chart, it has to be done via the

public void setData(ChartData data) { ... }

method. The baseclass ChartData (ChartData) class encapsulates all data and information that is needed for the chart during rendering. For each type of chart, a different subclass of ChartData (e.g. LineData) exists that should be used for setting data for the chart. In the constructor, you can hand over an List<? extends IDataSet> as the values to display. Below is an example with the class LineData (extends ChartData), which is used for adding data to a LineChart:

  /** List constructor */
    public LineData(List<ILineDataSet> sets) { ... }

    /** Constructor with one or multiple ILineDataSet objects */
    public LineData(ILineDataSet...) { ... }

So, what is a DataSet and why do you need it? That is actually pretty simple. One DataSet object represents a group of entries (e.g. class Entry) inside the chart that belong together. It is designed to logically separate different groups of values in the chart. For each type of chart, a differnt object that extends DataSet (e.g. LineDataSet) exists that allows specific styling.

As an example, you might want to display the quarterly revenue of two different companies over one year in a LineChart. In that case, it would be recommended to create two different LineDataSet objects, each containing four values (one for each quarter).

Of course, it is also possible to provide just one LineDataSet object containing all 8 values for the two companys.

So how to setup a LineDataSet object?

public LineDataSet(List<Entry> entries, String label) { ... }

When looking at the constructor (different constructors are available), it is visible that the LineDataSet needs an List of type Entry and a String used to describe the LineDataSet and as a label used for the Legend. Furthermore this label can be used to find the LineDataSet amongst other LineDataSet objects in the LineData object.

The List of type Entry encapsulates all values of the chart. A Entry object is an additional wrapper around an entry in the chart with a x- and y-value:

public Entry(float x, float y) { ... }

Putting it all together (example of two companies with quarterly revenue over one year):

At first, create the lists of type Entry that will hold your values:

List<Entry> valsComp1 = new ArrayList<Entry>();
    List<Entry> valsComp2 = new ArrayList<Entry>();

Then, fill the lists with Entry objects. Make sure the entry objects contain the correct indices to the x-axis. (of course, a loop can be used here, in that case, the counter variable of the loop could be the index on the x-axis).

Entry c1e1 = new Entry(0f, 100000f); // 0 == quarter 1
    valsComp1.add(c1e1);
    Entry c1e2 = new Entry(1f, 140000f); // 1 == quarter 2 ...
    valsComp1.add(c1e2);
    // and so on ...
    
    Entry c2e1 = new Entry(0f, 130000f); // 0 == quarter 1
    valsComp2.add(c2e1);
    Entry c2e2 = new Entry(1f, 115000f); // 1 == quarter 2 ...
    valsComp2.add(c2e2);
    //...

Now that we have our lists of Entry objects, the LineDataSet objects can be created:

LineDataSet setComp1 = new LineDataSet(valsComp1, "Company 1");
    setComp1.setAxisDependency(AxisDependency.LEFT);
    LineDataSet setComp2 = new LineDataSet(valsComp2, "Company 2");
    setComp2.setAxisDependency(AxisDependency.LEFT);

By calling setAxisDependency(...), the axis the DataSet should be plotted against is specified. Last but not least, we create a list of IDataSets and build our ChartData object:

// use the interface ILineDataSet
    List<ILineDataSet> dataSets = new ArrayList<ILineDataSet>();
    dataSets.add(setComp1);
    dataSets.add(setComp2);
    
    LineData data = new LineData(dataSets);
    mLineChart.setData(data);
    mLineChart.invalidate(); // refresh

After calling invalidate() the chart is refreshed and the provided data is drawn.

If we want to add more descriptive values to the x-axis (instead of numbers ranging from 0 to 3 for the different quarters), we can do so by using the IAxisValueFormatter interface. This interface allows custom styling of values drawn on the XAxis. In this example, the formatter could look like this:

// the labels that should be drawn on the XAxis 
// 应该在XAxis上绘制的标签
final String[] quarters = new String[] { "Q1", "Q2", "Q3", "Q4" };

IAxisValueFormatter formatter = new IAxisValueFormatter() {

    @Override
    public String getFormattedValue(float value, AxisBase axis) {
        return quarters[(int) value];
    }

    // we don't draw numbers, so no decimal digits needed
// 我们不绘制数字,因此不需要十进制数字
    @Override
    public int getDecimalDigits() {  return 0; }
};

XAxis xAxis = mLineChart.getXAxis();
xAxis.setGranularity(1f); // minimum axis-step (interval) is 1
xAxis.setValueFormatter(formatter);

Detailed information concerning the IAxisValueFormatter interface can be found here.

If additional styling is applied, the LineChart resulting from this example should look similar to the one below:

image.png

Setting data for normal BarChart, ScatterChart, BubbleChart and CandleStickChartworks similar to the LineChart. A special case is the BarChart with multiple (grouped) bars, which will be explained below.

The order of entries

Please be aware that this library does not officially support drawing LineChart data from an Entry list not sorted by the x-position of the entries in ascending manner. Adding entries in an unsorted way may result in correct drawing, but may also lead to unexpected behaviour. A Listof Entry objects can be sorted manually or using the EntryXComparator:

List<Entry> entries = ...;
Collections.sort(entries, new EntryXComparator());

The reason why this is required is because the library uses binary search algorithms for better performance only working on sorted lists.

BarChart

The way data can be set for a BarChart is very similar to the LineChart. The major difference are the data objects that need to be used for setting the data (e.g. BarEntry instead of Entry). In addition to that, the BarChart has different styling options.

Consider the following example of filling a BarChart with data:

List<BarEntry> entries = new ArrayList<>();
entries.add(new BarEntry(0f, 30f));
entries.add(new BarEntry(1f, 80f));
entries.add(new BarEntry(2f, 60f));
entries.add(new BarEntry(3f, 50f)); 
                                    // gap of 2f
entries.add(new BarEntry(5f, 70f));
entries.add(new BarEntry(6f, 60f));

BarDataSet set = new BarDataSet(entries, "BarDataSet");

In the above example, five BarEntry objects were created and added to a BarDataSet. Notice that there is a gap of "2" on the x-position between the fourth and fifth entry. In this example, this gap is used to explain how the positioning of bars in the BarChart works. The screenshot in the end of this tutorial will show the resulting BarChart for the given data. As a next step, a BarData object needs to be created:

BarData data = new BarData(set);
data.setBarWidth(0.9f); // set custom bar width
chart.setData(data);
chart.setFitBars(true); // make the x-axis fit exactly all bars
chart.invalidate(); // refresh

In the above snippet, a BarData object was created. When the BarEntry objects for the chart were created, we left a space of "1f" on the x-axis between (the centres) of each bar. By setting the bar-width to 0.9f, we effectively create a space of 0.1f between each bar. The setFitBars(true) call will tell the chart to adjust it's range of x-axis values to exactly fit all bars, and no bars are cut off on the sides.

After creating the BarData object, we set it to the chart and refresh. The result should look close to the one shown below:

image.png

Grouped BarChart

Since release v3.0.0, MPAndroidChart supports drawing bars explicitly grouped (in this case the library will handle the x-position), or user defined, which means that the user can place the bars anywhere he wants by altering the x-position of the bar.

This section will focus on explicit grouped BarChart, which means that the library will handle the x-positions of the bars. Consider the following example setup:

YourData[] group1 = ...;
YourData[] group2 = ...;

List<BarEntry> entriesGroup1 = new ArrayList<>();
List<BarEntry> entriesGroup2 = new ArrayList<>();

// fill the lists
for(int i = 0; i < group1.length; i++) {
    entriesGroup1.add(new BarEntry(i, group1.getValue()));
    entriesGroup2.add(new BarEntry(i, group2.getValue()));
}

BarDataSet set1 = new BarDataSet(entriesGroup1, "Group 1");
BarDataSet set2 = new BarDataSet(entriesGroup2, "Group 2");

In this example case, we will have two groups of bars, each represented by an individual BarDataSet. In case of explicit (library handled) groups, the actual x-position of the entries does not matter. Grouping is performed based on the position of the BarEntry in the entries List.

float groupSpace = 0.06f;
float barSpace = 0.02f; // x2 dataset
float barWidth = 0.45f; // x2 dataset
// (0.02 + 0.45) * 2 + 0.06 = 1.00 -> interval per "group"

BarData data = new BarData(set1, set2);
data.setBarWidth(barWidth); // set the width of each bar
barChart.setData(data);
barChart.groupBars(1980f, groupSpace, barSpace); // perform the "explicit" grouping
barChart.invalidate(); // refresh

In the code snippet above, the BarDataSet objects are added to a BarChart. The groupBars(...) method performs the grouping of the two BarDataSet objects. The method takes the following parameters:

public void groupBars(float fromX, float groupSpace, float barSpace) { ... }

The fromX parameter determines where on the XAxis the grouped bars should start (in this case "1980"), the groupSpace determines the space left in between each group of bars, and the barSpace determines the space between individual bars of a group. Based on these parameters, the groupBars(...) method alters the position of each bar on the XAxis towards a grouped appearance, preserving the order of the individual BarEntry objects.

The "interval" (occupied space) each group has on the XAxis is also defined by the groupSpace and barSpace parameters, and the barWidth.

The result should look somewhat like this:

image.png

Of course a grouped BarChart can also be achieved without the use of the groupBars(...) method, by simply positioning the individual bars correctly on the XAxis manually.

In order to make sure the labels of the XAxis are centered above the groups like in the screenshot above, you can use the setCenterAxisLabels(...) method:

XAxis xAxis = chart.getXAxis();
xAxis.setCenterAxisLabels(true);

Stacked BarChart

The stacked BarChart setup is completely similar to normal BarChart, with the exception being the way the individual BarEntry objects are created. In case of stacked bars, a different constructor for the BarEntry has to be used:

public BarEntry(float x, float [] yValues) { ... }

This constructor allows to provide multiple yValues, which represent the values of the "stack" of each bar. Consider the following example object:

BarEntry stackedEntry = new BarEntry(0f, new float[] { 10, 20, 30 });

This BarEntry has consists of a stack of three values, having a "height" of "10", "20" and "30".

PieChart

Unlike other chart types, the PieChart takes data in form of PieEntry objects. The constructor for these objects looks as follows:

public PieEntry(float value, String label) { ... }

The first parameter of the constructor is used for the actual "value" that should be drawn as a pie-slice in the PieChart. The second String parameter called "label" is used to provide additional description of the slice. Consider the following example PieChart setup:

List<PieEntry> entries = new ArrayList<>();

entries.add(new PieEntry(18.5f, "Green"));
entries.add(new PieEntry(26.7f, "Yellow"));
entries.add(new PieEntry(24.0f, "Red"));
entries.add(new PieEntry(30.8f, "Blue"));

PieDataSet set = new PieDataSet(entries, "Election Results");
PieData data = new PieData(set);
pieChart.setData(data);
pieChart.invalidate(); // refresh

The PieEntry objects to not hold a value for the x-position because the order of the PieEntry objects displayed in the chart is determined by their order in the entries list.

When adding some additional styling, the resulting PieChart with the data used above could look similar to this:

image.png
上一篇 下一篇

猜你喜欢

热点阅读