一、简介
点击查看创建自定义视图组件中文官网
Android 提供了一个复杂而强大的组件化模型,用于基于基本布局类 View 和 ViewGroup 构建界面。该平台包含各种预构建的 View 和 ViewGroup 子类(分别称为 widget 和布局),可供您用来构建界面。
可用布局包括ConstraintLayout、 LinearLayout、FrameLayout、RelativeLayout 等。
如果预构建的 widget 或布局都不能满足您的需求,您可以创建自己的 View 子类。如果您只需要对现有 widget 或布局进行细微调整,则可以创建相应 widget 或布局的子类并替换其方法。
通过创建自己的 View 子类,您可以精确控制屏幕元素的外观和功能。为了让您了解自定义视图可以实现哪些控制,下面列举了一些示例来说明您可以如何使用自定义视图:
- 您可以创建一个完全自定义渲染的 View 类型,例如,使用 2D 图形渲染的“音量控制”旋钮,类似于模拟电子控件。
- 您可以将一组 View 组件组合成一个新的组件,例如制作组合框(弹出式列表和自由输入文本字段的组合)、双窗格选择器控件(左右窗格,其中每个窗格都有一个列表,您可以在其中重新分配哪个列表中的项)等。
- 您可以替换 EditText 组件在屏幕上的渲染方式。 NotePad 示例应用使用此效果有效地创建了一个带线条的记事本页面。
- 您可以捕获其他事件(例如按键),并以自定义方式(例如在游戏中)处理这些事件。
二、基本方法
下面简要介绍了创建您自己的 View 组件需要了解的内容:
- 使用您自己的类扩展现有的 View 类或子类。
- 替换父类中的某些方法。要替换的父类方法以 on 开头,例如onLayout() 、onMeasure() 、 onDraw() 和 onKeyDown()。
- 使用您的新扩展类。完成后,您可以使用新的扩展类来代替其所基于的视图。
三、扩展 onDraw() 和 onMeasure()
onDraw() 方法提供了一个 Canvas,您可以在其上实现所需的任何内容:2D 图形、其他标准或自定义组件、样式文本或您能想到的任何其他内容。
注意:这不适用于 3D 图形。如果要使用 3D 图形,请扩展 SurfaceView(而非 View)并从单独的线程绘制。如需了解详情,请参阅 GLSurfaceViewActivity 示例。
onMeasure() 涉及更多。onMeasure() 是组件与其容器之间渲染协定的关键部分。必须替换 onMeasure(),才能高效且准确地报告其所含部分的测量结果。父级的限制要求(传递到 onMeasure() 方法)以及计算后使用测量的宽度和高度调用 setMeasuredDimension() 方法的要求,让这变得稍微有些复杂。如果您不通过已替换的 onMeasure() 方法调用此方法,则会导致测量时出现异常。
概括来讲,实现 onMeasure() 如下所示:
- 系统会使用宽度和高度规范调用替换的 onMeasure() 方法,这些规范被视为对您生成的宽度和高度的限制要求。widthMeasureSpec 和 heightMeasureSpec 参数都是表示维度的整数代码。有关这些规范可能要求的限制的完整参考,请参阅 View.onMeasure(int, int) 下的参考文档。此参考文档还介绍了整个测量操作。
- 组件的 onMeasure() 方法会计算渲染组件所需的测量宽度和高度。它必须尽量符合传入的规范,但也可能会超过这些规范。在这种情况下,父级可以选择要执行的操作,包括裁剪、滚动、抛出异常或要求 onMeasure() 重试,或许使用不同的测量规范。
- 计算宽度和高度后,使用计算出的测量值调用 setMeasuredDimension(int width, int height) 方法。否则会导致异常。
四、自定义View的绘制流程
4.1 创建
- 构造函数
构造函数有两种形式:从代码创建视图时调用,另一种形式在从布局文件膨胀视图时调用。第二种形式解析并应用布局文件中定义的属性。- onFinishInflate()
在视图及其所有子项都从 XML 扩充之后调用。
4.2 布局
- onMeasure(int, int)
调用以确定此视图及其所有子级的大小要求。- onLayout(boolean, int, int, int, int)
在此视图必须为其所有子视图分配大小和位置时调用。- onSizeChanged(int, int, int, int)
在此视图的大小发生更改时调用。
4.3 绘制
- onDraw(Canvas)
在视图必须渲染其内容时调用。
4.4 事件处理
- onKeyDown(int, KeyEvent)
在发生按键按下事件时调用。- onKeyUp(int, KeyEvent)
在发生 key up 事件时调用- onTrackballEvent(MotionEvent)
在发生轨迹球动作事件时调用。- onTouchEvent(MotionEvent)
在发生触摸屏动作事件时调用。
4.5 侧重点
- onFocusChanged(boolean, int, Rect)
在视图获得或失去焦点时调用。- onWindowFocusChanged(boolean)
在包含视图的窗口获得或失去焦点时调用。
4.6 附加
- onAttachedToWindow()
在视图附加到窗口时调用。- onDetachedFromWindow()
在视图与其窗口分离时调用。- onWindowVisibilityChanged(int)
在包含视图的窗口的可见性发生更改时调用。
五、复合控件
如果您不想创建完全自定义的组件,而是希望将可重复使用的组件(由一组现有控件组成)组合在一起,那么创建复合组件(或复合控件)可能是最好的选择。总而言之,这会将许多原子性控件或视图整合到可视为一项的逻辑项组中。 例如,组合框可以是单行 EditText 字段和附有弹出式列表的相邻按钮的组合。如果用户点按该按钮并从列表中选择了内容,系统会填充 EditText 字段,但用户也可以根据需要直接在 EditText 中输入内容。
在 Android 中,还有另外两个视图可用于执行此操作:Spinner 和 AutoCompleteTextView。无论如何,这个组合框概念都是一个很好的例子。
如需创建复合组件,请执行以下操作:
- 与 Activity 一样,使用声明式(基于 XML)方法创建包含的组件,或者以程序化方式从代码中嵌套组件。通常的起点是某种类型的 Layout,因此请创建一个扩展 Layout 的类。对于组合框,您可以使用水平方向的 LinearLayout。您可以在里面嵌套其他布局,使复合组件可以任意复杂化和结构化。
- 在新类的构造函数中,获取父类所需的任何参数,并先将它们传递给父类构造函数。然后,您可以设置其他视图,以便在新组件中使用。您可以在这里创建 EditText 字段和弹出式列表。您可以在 XML 中引入您自己的属性和参数,以便构造函数可以提取和使用。
- (可选)为包含的视图可能生成的事件创建监听器。例如,在选择了列表的情况下,列表项点击监听器会更新 EditText 的内容。
- (可选)使用访问器和修饰符创建自己的属性。例如,最初在组件中设置 EditText 值,并在需要时查询其内容。
- (可选)替换 onDraw() 和 onMeasure()。在扩展 Layout 时通常没有必要这样做,因为布局具有可能正常运行的默认行为。
- (可选)替换其他 on 方法(如 onKeyDown()),例如,在点按某个键时,从组合框的弹出式列表中选择特定默认值。
使用 Layout 作为自定义控件的基础具有如下优势:
- 您可以使用声明式 XML 文件指定布局(就像使用 activity 屏幕一样),也可以以编程方式创建视图并将其从代码嵌套到布局中。
- onDraw() 和 onMeasure() 方法以及大多数其他 on 方法具有合适的行为,因此您无需替换它们。
- 您可以快速构建任意复杂的复合视图,并像使用单个组件一样重复使用它们。
六、修改现有视图类型
如果存在与您所需的组件类似的组件,您可以扩展该组件并替换您想要更改的行为。您可以使用完全自定义的组件执行所有操作,但通过从 View 层次结构中更专用的类着手,您可以免费获得一些执行您所需要的行为。
例如,NotePad 示例应用演示了使用 Android 平台的多个方面。其中包括扩展 EditText 视图,使之成为带线条的记事本。这并不是一个完美的示例,并且用于执行此操作的 API 可能会发生变化,但它演示了相关原则。
如果您尚未执行此操作,请将 NotePad 示例导入 Android Studio,或使用提供的链接查看源代码。请特别留意 NoteEditor.java 文件中 LinedEditText 的定义。
下面是此文件中的一些注意事项:
- 定义
该类使用以下行进行定义:
public static class LinedEditText extends EditText
LinedEditText 定义为 NoteEditor Activity 中的一个内部类,但它是公共类,因此可以作为 NoteEditor.LinedEditText 从 NoteEditor 类外部进行访问。
此外,LinedEditText 为 static,这意味着它不会生成允许其访问父类数据的所谓“合成方法”。这意味着它的行为表现为一个单独的类,而不是与 NoteEditor 密切相关的类。如果内部类不需要从外部类访问状态,则这是一种更简洁的方法来创建内部类。它使生成的类保持较小,并便于其他类使用。
LinedEditText 扩展了 EditText(在本例中是要自定义的视图)。完成后,新类可以代替普通的 EditText 视图。- 类初始化
与往常一样,首先调用父类。这不是默认构造函数,而是参数化构造函数。EditText 在从 XML 布局文件膨胀时是使用这些参数创建的。因此,构造函数需要获取这些方法并将其传递给父类构造函数。- 替换的方法
此示例仅替换 onDraw() 方法,但您可能需要在创建自己的自定义组件时替换其他方法。
在此示例中,通过替换 onDraw() 方法,您可以在 EditText 视图画布上绘制蓝色线条。系统会将画布传入被替换的 onDraw() 方法。系统会在 super.onDraw() 方法结束之前调用该方法。必须调用父类方法。在本例中,请在绘制要包含的行后调用该函数。- 自定义组件
现在,您已经有了自定义组件,但如何使用它呢?在记事本示例中,自定义组件直接从声明式布局中使用,因此请查看 res/layout 文件夹中的 note_editor.xml:
<view xmlns:android="http://schemas.android.com/apk/res/android"
class="com.example.android.notepad.NoteEditor$LinedEditText"
android:id="@+id/note"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:padding="5dp"
android:scrollbars="vertical"
android:fadingEdge="vertical"
android:gravity="top"
android:textSize="22sp"
android:capitalize="sentences"
/>
将自定义组件创建为 XML 中的通用视图,并使用完整软件包指定类。您定义的内部类使用 NoteEditor$LinedEditText 表示法引用,这是以 Java 编程语言引用内部类的标准方式。
如果您的自定义视图组件未定义为内部类,您可以使用 XML 元素名称声明视图组件,并排除 class 属性。例如:
<com.example.android.notepad.LinedEditText
id="@+id/note"
... />
请注意,LinedEditText 类现在是一个单独的类文件。当该类嵌套在 NoteEditor 类中时,此方法不起作用。
定义中的其他属性和参数是传入自定义组件构造函数中和再传递到 EditText 构造函数的,因此它们与您用于 EditText 视图的参数相同。您也可以添加自己的参数。
七、效果图
八、NoteEditor代码
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.notepad;
import android.app.Activity;
import android.app.LoaderManager;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
import android.content.Loader;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.EditText;
import com.example.android.notepad.NotePad.Notes;
/**
* This Activity handles "editing" a note, where editing is responding to
* {@link Intent#ACTION_VIEW} (request to view data), edit a note
* {@link Intent#ACTION_EDIT}, create a note {@link Intent#ACTION_INSERT}, or
* create a new note from the current contents of the clipboard {@link Intent#ACTION_PASTE}.
*/
public class NoteEditor extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {
// For logging and debugging purposes
private static final String TAG = "NoteEditor";
/*
* Creates a projection that returns the note ID and the note contents.
*/
private static final String[] PROJECTION =
new String[] {
NotePad.Notes._ID,
NotePad.Notes.COLUMN_NAME_TITLE,
NotePad.Notes.COLUMN_NAME_NOTE
};
// A label for the saved state of the activity
private static final String ORIGINAL_CONTENT = "origContent";
// This Activity can be started by more than one action. Each action is represented
// as a "state" constant
private static final int STATE_EDIT = 0;
private static final int STATE_INSERT = 1;
private static final int LOADER_ID = 1;
// Global mutable variables
private int mState;
private Uri mUri;
private EditText mText;
private String mOriginalContent;
/**
* Defines a custom EditText View that draws lines between each line of text that is displayed.
*/
public static class LinedEditText extends EditText {
private Rect mRect;
private Paint mPaint;
// This constructor is used by LayoutInflater
public LinedEditText(Context context, AttributeSet attrs) {
super(context, attrs);
// Creates a Rect and a Paint object, and sets the style and color of the Paint object.
mRect = new Rect();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0x800000FF);
}
/**
* This is called to draw the LinedEditText object
* @param canvas The canvas on which the background is drawn.
*/
@Override
protected void onDraw(Canvas canvas) {
// Gets the number of lines of text in the View.
int count = getLineCount();
// Gets the global Rect and Paint objects
Rect r = mRect;
Paint paint = mPaint;
/*
* Draws one line in the rectangle for every line of text in the EditText
*/
for (int i = 0; i < count; i++) {
// Gets the baseline coordinates for the current line of text
int baseline = getLineBounds(i, r);
/*
* Draws a line in the background from the left of the rectangle to the right,
* at a vertical position one dip below the baseline, using the "paint" object
* for details.
*/
canvas.drawLine(r.left, baseline + 1, r.right, baseline + 1, paint);
}
// Finishes up by calling the parent method
super.onDraw(canvas);
}
}
/**
* This method is called by Android when the Activity is first started. From the incoming
* Intent, it determines what kind of editing is desired, and then does it.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Recovering the instance state from a previously destroyed Activity instance
if (savedInstanceState != null) {
mOriginalContent = savedInstanceState.getString(ORIGINAL_CONTENT);
}
/*
* Creates an Intent to use when the Activity object's result is sent back to the
* caller.
*/
final Intent intent = getIntent();
/*
* Sets up for the edit, based on the action specified for the incoming Intent.
*/
// Gets the action that triggered the intent filter for this Activity
final String action = intent.getAction();
// For an edit action:
if (Intent.ACTION_EDIT.equals(action)) {
// Sets the Activity state to EDIT, and gets the URI for the data to be edited.
mState = STATE_EDIT;
mUri = intent.getData();
// For an insert or paste action:
} else if (Intent.ACTION_INSERT.equals(action)
|| Intent.ACTION_PASTE.equals(action)) {
// Sets the Activity state to INSERT, gets the general note URI, and inserts an
// empty record in the provider
mState = STATE_INSERT;
setTitle(getText(R.string.title_create));
mUri = getContentResolver().insert(intent.getData(), null);
/*
* If the attempt to insert the new note fails, shuts down this Activity. The
* originating Activity receives back RESULT_CANCELED if it requested a result.
* Logs that the insert failed.
*/
if (mUri == null) {
// Writes the log identifier, a message, and the URI that failed.
Log.e(TAG, "Failed to insert new note into " + getIntent().getData());
// Closes the activity.
finish();
return;
}
// Since the new entry was created, this sets the result to be returned
// set the result to be returned.
setResult(RESULT_OK, (new Intent()).setAction(mUri.toString()));
// If the action was other than EDIT or INSERT:
} else {
// Logs an error that the action was not understood, finishes the Activity, and
// returns RESULT_CANCELED to an originating Activity.
Log.e(TAG, "Unknown action, exiting");
finish();
return;
}
// Initialize the LoaderManager and start the query
getLoaderManager().initLoader(LOADER_ID, null, this);
// For a paste, initializes the data from clipboard.
if (Intent.ACTION_PASTE.equals(action)) {
// Does the paste
performPaste();
// Switches the state to EDIT so the title can be modified.
mState = STATE_EDIT;
}
// Sets the layout for this Activity. See res/layout/note_editor.xml
setContentView(R.layout.note_editor);
// Gets a handle to the EditText in the the layout.
mText = (EditText) findViewById(R.id.note);
}
/**
* This method is called when an Activity loses focus during its normal operation.
* The Activity has a chance to save its state so that the system can restore
* it.
*
* Notice that this method isn't a normal part of the Activity lifecycle. It won't be called
* if the user simply navigates away from the Activity.
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
// Save away the original text, so we still have it if the activity
// needs to be re-created.
outState.putString(ORIGINAL_CONTENT, mOriginalContent);
// Call the superclass to save the any view hierarchy state
super.onSaveInstanceState(outState);
}
/**
* This method is called when the Activity loses focus.
*
* While there is no need to override this method in this app, it is shown here to highlight
* that we are not saving any state in onPause, but have moved app state saving to onStop
* callback.
* In earlier versions of this app and popular literature it had been shown that onPause is good
* place to persist any unsaved work, however, this is not really a good practice because of how
* application and process lifecycle behave.
* As a general guideline apps should have a way of saving their business logic that does not
* solely rely on Activity (or other component) lifecyle state transitions.
* As a backstop you should save any app state, not saved during lifetime of the Activity, in
* onStop().
* For a more detailed explanation of this recommendation please read
* <a href = "https://developer.android.com/guide/topics/processes/process-lifecycle.html">
* Processes and Application Life Cycle </a>.
* <a href="https://developer.android.com/training/basics/activity-lifecycle/pausing.html">
* Pausing and Resuming an Activity </a>.
*/
@Override
protected void onPause() {
super.onPause();
}
/**
* This method is called when the Activity becomes invisible.
*
* For Activity objects that edit information, onStop() may be the one place where changes maybe
* saved.
*
* If the user hasn't done anything, then this deletes or clears out the note, otherwise it
* writes the user's work to the provider.
*/
@Override
protected void onStop() {
super.onStop();
// Get the current note text.
String text = mText.getText().toString();
int length = text.length();
/*
* If the Activity is in the midst of finishing and there is no text in the current
* note, returns a result of CANCELED to the caller, and deletes the note. This is done
* even if the note was being edited, the assumption being that the user wanted to
* "clear out" (delete) the note.
*/
if (isFinishing() && (length == 0)) {
setResult(RESULT_CANCELED);
deleteNote();
/*
* Writes the edits to the provider. The note has been edited if an existing note
* was retrieved into the editor *or* if a new note was inserted.
* In the latter case, onCreate() inserted a new empty note into the provider,
* and it is this new note that is being edited.
*/
} else if (mState == STATE_EDIT) {
// Creates a map to contain the new values for the columns
updateNote(text, null);
} else if (mState == STATE_INSERT) {
updateNote(text, text);
mState = STATE_EDIT;
}
}
/**
* This method is called when the user clicks the device's Menu button the first time for
* this Activity. Android passes in a Menu object that is populated with items.
*
* Builds the menus for editing and inserting, and adds in alternative actions that
* registered themselves to handle the MIME types for this application.
*
* @param menu A Menu object to which items should be added.
* @return True to display the menu.
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate menu from XML resource
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.editor_options_menu, menu);
// Only add extra menu items for a saved note
if (mState == STATE_EDIT) {
// Append to the
// menu items for any other activities that can do stuff with it
// as well. This does a query on the system for any activities that
// implement the ALTERNATIVE_ACTION for our data, adding a menu item
// for each one that is found.
Intent intent = new Intent(null, mUri);
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0,
new ComponentName(this, NoteEditor.class), null, intent, 0, null);
}
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// Check if note has changed and enable/disable the revert option
Cursor cursor = getContentResolver().query(
mUri, // The URI for the note that is to be retrieved.
PROJECTION, // The columns to retrieve
null, // No selection criteria are used, so no where columns are needed.
null, // No where columns are used, so no where values are needed.
null // No sort order is needed.
);
cursor.moveToFirst();
int colNoteIndex = cursor.getColumnIndex(Notes.COLUMN_NAME_NOTE);
String savedNote = cursor.getString(colNoteIndex);
String currentNote = mText.getText().toString();
if (savedNote.equals(currentNote)) {
menu.findItem(R.id.menu_revert).setVisible(false);
} else {
menu.findItem(R.id.menu_revert).setVisible(true);
}
return super.onPrepareOptionsMenu(menu);
}
/**
* This method is called when a menu item is selected. Android passes in the selected item.
* The switch statement in this method calls the appropriate method to perform the action the
* user chose.
*
* @param item The selected MenuItem
* @return True to indicate that the item was processed, and no further work is necessary. False
* to proceed to further processing as indicated in the MenuItem object.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle all of the possible menu actions.
switch (item.getItemId()) {
case R.id.menu_save:
String text = mText.getText().toString();
updateNote(text, null);
finish();
break;
case R.id.menu_delete:
deleteNote();
finish();
break;
case R.id.menu_revert:
cancelNote();
break;
}
return super.onOptionsItemSelected(item);
}
//BEGIN_INCLUDE(paste)
/**
* A helper method that replaces the note's data with the contents of the clipboard.
*/
private final void performPaste() {
// Gets a handle to the Clipboard Manager
ClipboardManager clipboard = (ClipboardManager)
getSystemService(Context.CLIPBOARD_SERVICE);
// Gets a content resolver instance
ContentResolver cr = getContentResolver();
// Gets the clipboard data from the clipboard
ClipData clip = clipboard.getPrimaryClip();
if (clip != null) {
String text=null;
String title=null;
// Gets the first item from the clipboard data
ClipData.Item item = clip.getItemAt(0);
// Tries to get the item's contents as a URI pointing to a note
Uri uri = item.getUri();
// Tests to see that the item actually is an URI, and that the URI
// is a content URI pointing to a provider whose MIME type is the same
// as the MIME type supported by the Note pad provider.
if (uri != null && NotePad.Notes.CONTENT_ITEM_TYPE.equals(cr.getType(uri))) {
// The clipboard holds a reference to data with a note MIME type. This copies it.
Cursor orig = cr.query(
uri, // URI for the content provider
PROJECTION, // Get the columns referred to in the projection
null, // No selection variables
null, // No selection variables, so no criteria are needed
null // Use the default sort order
);
// If the Cursor is not null, and it contains at least one record
// (moveToFirst() returns true), then this gets the note data from it.
if (orig != null) {
if (orig.moveToFirst()) {
int colNoteIndex = orig.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE);
int colTitleIndex = orig.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE);
text = orig.getString(colNoteIndex);
title = orig.getString(colTitleIndex);
}
// Closes the cursor.
orig.close();
}
}
// If the contents of the clipboard wasn't a reference to a note, then
// this converts whatever it is to text.
if (text == null) {
text = item.coerceToText(this).toString();
}
// Updates the current note with the retrieved title and text.
updateNote(text, title);
}
}
//END_INCLUDE(paste)
/**
* Replaces the current note contents with the text and title provided as arguments.
* @param text The new note contents to use.
* @param title The new note title to use
*/
private final void updateNote(String text, String title) {
// Sets up a map to contain values to be updated in the provider.
ContentValues values = new ContentValues();
values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, System.currentTimeMillis());
// If the action is to insert a new note, this creates an initial title for it.
if (mState == STATE_INSERT) {
// If no title was provided as an argument, create one from the note text.
if (title == null) {
// Get the note's length
int length = text.length();
// Sets the title by getting a substring of the text that is 31 characters long
// or the number of characters in the note plus one, whichever is smaller.
title = text.substring(0, Math.min(30, length));
// If the resulting length is more than 30 characters, chops off any
// trailing spaces
if (length > 30) {
int lastSpace = title.lastIndexOf(' ');
if (lastSpace > 0) {
title = title.substring(0, lastSpace);
}
}
}
// In the values map, sets the value of the title
values.put(NotePad.Notes.COLUMN_NAME_TITLE, title);
} else if (title != null) {
// In the values map, sets the value of the title
values.put(NotePad.Notes.COLUMN_NAME_TITLE, title);
}
// This puts the desired notes text into the map.
values.put(NotePad.Notes.COLUMN_NAME_NOTE, text);
/*
* Updates the provider with the new values in the map. The ListView is updated
* automatically. The provider sets this up by setting the notification URI for
* query Cursor objects to the incoming URI. The content resolver is thus
* automatically notified when the Cursor for the URI changes, and the UI is
* updated.
* Note: This is being done on the UI thread. It will block the thread until the
* update completes. In a sample app, going against a simple provider based on a
* local database, the block will be momentary, but in a real app you should use
* android.content.AsyncQueryHandler or android.os.AsyncTask.
*/
getContentResolver().update(
mUri, // The URI for the record to update.
values, // The map of column names and new values to apply to them.
null, // No selection criteria are used, so no where columns are necessary.
null // No where columns are used, so no where arguments are necessary.
);
}
/**
* This helper method cancels the work done on a note. It deletes the note if it was
* newly created, or reverts to the original text of the note i
*/
private final void cancelNote() {
if (mState == STATE_EDIT) {
// Put the original note text back into the database
ContentValues values = new ContentValues();
values.put(NotePad.Notes.COLUMN_NAME_NOTE, mOriginalContent);
getContentResolver().update(mUri, values, null, null);
} else if (mState == STATE_INSERT) {
// We inserted an empty note, make sure to delete it
deleteNote();
}
setResult(RESULT_CANCELED);
finish();
}
/**
* Take care of deleting a note. Simply deletes the entry.
*/
private final void deleteNote() {
getContentResolver().delete(mUri, null, null);
mText.setText("");
}
// LoaderManager callbacks
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
return new CursorLoader(
this,
mUri, // The URI for the note that is to be retrieved.
PROJECTION, // The columns to retrieve
null, // No selection criteria are used, so no where columns are needed.
null, // No where columns are used, so no where values are needed.
null // No sort order is needed.
);
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
// Modifies the window title for the Activity according to the current Activity state.
if (cursor != null && cursor.moveToFirst() && mState == STATE_EDIT) {
// Set the title of the Activity to include the note title
int colTitleIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE);
int colNoteIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE);
// Gets the title and sets it
String title = cursor.getString(colTitleIndex);
Resources res = getResources();
String text = String.format(res.getString(R.string.title_edit), title);
setTitle(text);
// Gets the note text from the Cursor and puts it in the TextView, but doesn't change
// the text cursor's position.
String note = cursor.getString(colNoteIndex);
mText.setTextKeepState(note);
// Stores the original note text, to allow the user to revert changes.
if (mOriginalContent == null) {
mOriginalContent = note;
}
}
}
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {}
}