apuntes:android
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
apuntes:android [28/07/2020 07:53] – [Práctica 1.2] Santiago Faci | apuntes:android [30/11/2023 18:28] (current) – [Hacer una foto] Santiago Faci | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Programación de móviles con Android ====== | + | ======= Programación de móviles con Android |
- | ===== ¿Qué es Android ===== | + | {{ android-logo.jpg? |
+ | |||
+ | ====== ¿Qué es Android | ||
Actualmente cuando se habla de //Android// se hace tanto para referirse al Sistema Operativo que viene instalado en nuestros móviles como para hablar del framework que se utilizar para crear las aplicaciones. Conviene por tanto distinguir claramente entre //Android// que será el Sistema Operativo y //Android SDK// que es el framework ó SDK (Software Development Kit) utilizado para desarrollar las aplicaciones que funciona sobre dicho S.O. | Actualmente cuando se habla de //Android// se hace tanto para referirse al Sistema Operativo que viene instalado en nuestros móviles como para hablar del framework que se utilizar para crear las aplicaciones. Conviene por tanto distinguir claramente entre //Android// que será el Sistema Operativo y //Android SDK// que es el framework ó SDK (Software Development Kit) utilizado para desarrollar las aplicaciones que funciona sobre dicho S.O. | ||
Line 21: | Line 23: | ||
{{ youtube> | {{ youtube> | ||
\\ | \\ | ||
- | ===== Estructura de Android ===== | + | ====== Estructura de Android |
- | ==== Estructura del Sistema Operativo ==== | + | ===== Estructura del Sistema Operativo |
{{ android-stack_2x.png? | {{ android-stack_2x.png? | ||
- | ==== Ciclo de vida de una aplicación ===== | + | ===== Ciclo de vida de una aplicación |
{{ activity_lifecycle.png |Ciclo de vida Android}} | {{ activity_lifecycle.png |Ciclo de vida Android}} | ||
Line 32: | Line 34: | ||
El [[https:// | El [[https:// | ||
- | ==== Estructura de una aplicación ==== | + | ===== Estructura de una aplicación |
< | < | ||
Line 49: | Line 51: | ||
* **build.gradle** Es el fichero de configuración de //gradle// para nuestro proyecto de aplicación //Android// | * **build.gradle** Es el fichero de configuración de //gradle// para nuestro proyecto de aplicación //Android// | ||
- | === Código de la aplicación === | + | ==== Código de la aplicación |
El código de la aplicación debe ser empaquetado siguiendo la misma jerarquía y nombrado de paquetes que indican las [[http:// | El código de la aplicación debe ser empaquetado siguiendo la misma jerarquía y nombrado de paquetes que indican las [[http:// | ||
Line 55: | Line 57: | ||
Es especialmente importante seguir las reglas para el nombrado de paquetes y que además está coincida con la que se especifica al inicio del proyecto y que quedará registrada en el fichero de manifiesto '' | Es especialmente importante seguir las reglas para el nombrado de paquetes y que además está coincida con la que se especifica al inicio del proyecto y que quedará registrada en el fichero de manifiesto '' | ||
- | === Recursos de imagen (drawable) === | + | ==== Recursos de imagen (drawable) |
- | === Recursos de layout (layouts) === | + | ==== Recursos de layout (layouts) |
Aqui se almacenan los ficheros que contienen los layouts (diseños) de la aplicación. Lo más habitual será encontrar un layout por cada '' | Aqui se almacenan los ficheros que contienen los layouts (diseños) de la aplicación. Lo más habitual será encontrar un layout por cada '' | ||
- | === Recursos de idioma (values) === | + | ==== Recursos de idioma (values) |
Aquí se almacenan los ficheros que contienen todos los textos de la aplicación. El objetivo es separar totalmente el código de la aplicación del texto con la finalidad de simplificar la internacionalización de la misma. De esa manera, cuando se quiera traducir la aplicación a un nuevo idioma bastará con crear el fichero correspondiente con los textos en dicho idioma y no será necesario realizar ningún cambio en el código. | Aquí se almacenan los ficheros que contienen todos los textos de la aplicación. El objetivo es separar totalmente el código de la aplicación del texto con la finalidad de simplificar la internacionalización de la misma. De esa manera, cuando se quiera traducir la aplicación a un nuevo idioma bastará con crear el fichero correspondiente con los textos en dicho idioma y no será necesario realizar ningún cambio en el código. | ||
Line 181: | Line 183: | ||
</ | </ | ||
- | === Fichero de manifiesto AndroidManifest.xml === | + | ==== Fichero de manifiesto AndroidManifest.xml |
A continuación se muestra como ejemplo un fichero '' | A continuación se muestra como ejemplo un fichero '' | ||
Line 227: | Line 229: | ||
* ''< | * ''< | ||
* ''< | * ''< | ||
- | ===== Hello World! ===== | ||
- | ==== Estructura de un proyecto de ejemplo ==== | ||
- | ===== Componentes Android ===== | ||
- | ==== Componentes UI ==== | ||
- | === Layouts | + | ====== Componentes Android ====== |
- | Los layouts permiten distribuir todos los componentes que forma la GUI de una Activity en la pantalla. Principalmente usaremos dos: | + | ===== Layouts ===== |
- | * **LinearLayout (Horizontal|Vertical)** Permiten distribuir | + | Los layouts definen la forma en que se distribuyen |
+ | |||
+ | Todo layout puede ser considerado también | ||
+ | |||
+ | Los principales layouts actualmente son: | ||
+ | |||
+ | === ConstraintLayout === | ||
+ | |||
+ | Es el layout por defecto cuando se crea una nueva // | ||
< | < | ||
- | {{ linearlayout.png }} | + | {{ constraintlayout.png}} |
- | < | + | < |
+ | |||
+ | === LinearLayout === | ||
+ | |||
+ | Layout sencillo para alinear verticalmente/ | ||
< | < | ||
- | {{ linear_layout_vertical_horizontal.png }} | + | {{ linearLayout.png}} |
- | < | + | < |
+ | |||
+ | === FrameLayout === | ||
- | * **RelativeLayout** | + | Permite deliminar una zona donde se pueden colocar componentes superpuestos |
< | < | ||
- | {{ relative_layout.png }} | + | {{ FrameLayout.png}} |
- | < | + | < |
- | === TextView === | + | ===== TextView |
< | < | ||
Line 262: | Line 274: | ||
</ | </ | ||
- | === EditText === | + | ===== EditText |
< | < | ||
Line 269: | Line 281: | ||
</ | </ | ||
- | === Button === | + | ===== Button |
< | < | ||
Line 275: | Line 287: | ||
< | < | ||
- | === Spinner === | + | === RadioButton === |
+ | |||
+ | < | ||
+ | {{ radiobutton.png }} | ||
+ | < | ||
+ | |||
+ | === CheckBox === | ||
+ | |||
+ | < | ||
+ | {{ checkbox.png }} | ||
+ | < | ||
+ | |||
+ | ===== Spinner | ||
Un '' | Un '' | ||
Line 285: | Line 309: | ||
En cuanto al funcionamiento interno, es muy similar a un '' | En cuanto al funcionamiento interno, es muy similar a un '' | ||
- | === ListView === | + | ===== ListView |
+ | |||
+ | Ver el apartado sobre [[https:// | ||
Es un componente que permite crear una lista de elementos más o menos compleja, muy similar a '' | Es un componente que permite crear una lista de elementos más o menos compleja, muy similar a '' | ||
Line 484: | Line 510: | ||
* [[https:// | * [[https:// | ||
- | === RadioButton | + | ===== RecyclerView ===== |
- | < | + | '' |
- | {{ radiobutton.png }} | + | |
- | < | + | |
- | === CheckBox === | + | Primero, definimos la clase Java que modela el objeto con el que vamos a trabajar. En el caso de que fuéramos a listar simples '' |
- | <figure> | + | <code java> |
- | {{ checkbox.png }} | + | public class Superhero |
- | < | + | |
+ | private String surname; | ||
+ | private String superHeroeName; | ||
- | ==== Otros componentes ==== | + | public Superhero(String name, String surname, String superHeroeName) { |
+ | this.name | ||
+ | this.surname | ||
+ | this.superHeroeName | ||
+ | } | ||
- | === ActionBar === | + | public String getName() { |
+ | return name; | ||
+ | } | ||
+ | |||
+ | public String getSurname() { | ||
+ | return surname; | ||
+ | } | ||
+ | |||
+ | public String getSuperHeroeName() { | ||
+ | return superHeroeName; | ||
+ | } | ||
+ | |||
+ | public String getFullName() { | ||
+ | | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | La siguiente clase es el //Customer Adapter// que define cómo hay que renderizar cada elemento de la lista. En este caso, para cada super héroe, queremos mostrar 2 líneas con su nombre y apellidos, y una tercera con el botón para eliminar dicho elemento de la lista. | ||
+ | Además, la lista permite marcar como seleccionado (modificando su color de fondo) el elemento seleccionado, | ||
+ | |||
+ | <code java> | ||
+ | public class SuperheroAdapter extends RecyclerView.Adapter< | ||
+ | private List< | ||
+ | private int selectedPosition; | ||
+ | |||
+ | public SuperheroAdapter(List< | ||
+ | this.dataList = dataList; | ||
+ | selectedPosition = -1; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public SuperheroHolder onCreateViewHolder(ViewGroup parent, int viewType) { | ||
+ | View view = LayoutInflater.from(parent.getContext()) | ||
+ | .inflate(R.layout.character_item, | ||
+ | return new SuperheroHolder(view); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void onBindViewHolder(SuperheroHolder holder, int position) { | ||
+ | holder.tvFullName.setText(dataList.get(position).getFullName()); | ||
+ | holder.tvSuperheroName.setText(dataList.get(position).getSuperHeroeName()); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public int getItemCount() { | ||
+ | return dataList.size(); | ||
+ | } | ||
+ | |||
+ | public class SuperheroHolder extends RecyclerView.ViewHolder { | ||
+ | public TextView tvFullName; | ||
+ | public TextView tvSuperheroName; | ||
+ | public Button button; | ||
+ | public View parentView; | ||
+ | |||
+ | public SuperheroHolder(View view) { | ||
+ | super(view); | ||
+ | parentView = view; | ||
+ | |||
+ | tvFullName = view.findViewById(R.id.tvFullName); | ||
+ | tvSuperheroName = view.findViewById(R.id.tvSuperheroName); | ||
+ | button = view.findViewById(R.id.button); | ||
+ | |||
+ | // Click on superhero (select/ | ||
+ | view.setOnClickListener(view1 -> selectSuperhero(parentView, | ||
+ | // Click on button (remove superhero from the list) | ||
+ | button.setOnClickListener(view12 -> deleteSuperhero(getAdapterPosition())); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | private void selectSuperhero(View parentView, View view, int position) { | ||
+ | // Select / Unselect | ||
+ | if (getSelectedPosition() != position) { | ||
+ | // Only single selection is allowed | ||
+ | if (selectedPosition != -1) | ||
+ | return; | ||
+ | |||
+ | parentView.setBackgroundColor(view.getContext().getResources().getColor( | ||
+ | | ||
+ | selectedPosition = position; | ||
+ | // FIXME eliminar estos Toasts | ||
+ | Toast.makeText(view.getContext(), | ||
+ | Toast.LENGTH_SHORT).show(); | ||
+ | } else { | ||
+ | parentView.setBackgroundColor(view.getContext().getResources().getColor( | ||
+ | android.R.color.white)); | ||
+ | selectedPosition = -1; | ||
+ | // FIXME eliminar estos Toasts | ||
+ | Toast.makeText(view.getContext(), | ||
+ | Toast.LENGTH_SHORT).show(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | private void deleteSuperhero(int position) { | ||
+ | dataList.remove(position); | ||
+ | notifyItemRemoved(position); | ||
+ | } | ||
+ | |||
+ | public int getSelectedPosition() { | ||
+ | return selectedPosition; | ||
+ | } | ||
+ | |||
+ | public Superhero getSelectedSuperhero() { | ||
+ | if (getSelectedPosition() == -1) | ||
+ | return null; | ||
+ | |||
+ | return dataList.get(getSelectedPosition()); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Por último, en la '' | ||
+ | |||
+ | <code java> | ||
+ | public class MainActivity extends AppCompatActivity implements View.OnClickListener { | ||
+ | |||
+ | private List< | ||
+ | private SuperheroAdapter adapter; | ||
+ | |||
+ | @Override | ||
+ | protected void onCreate(Bundle savedInstanceState) { | ||
+ | super.onCreate(savedInstanceState); | ||
+ | setContentView(R.layout.activity_main); | ||
+ | |||
+ | populateSuperheroList(); | ||
+ | |||
+ | RecyclerView recyclerView = findViewById(R.id.recylerView); | ||
+ | recyclerView.setHasFixedSize(true); | ||
+ | LinearLayoutManager layoutManager = new LinearLayoutManager(this); | ||
+ | recyclerView.setLayoutManager(layoutManager); | ||
+ | adapter = new SuperheroAdapter(superheroList); | ||
+ | recyclerView.setAdapter(adapter); | ||
+ | |||
+ | Button continueButton = findViewById(R.id.continueButton); | ||
+ | continueButton.setOnClickListener(this); | ||
+ | } | ||
+ | |||
+ | private void populateSuperheroList() { | ||
+ | superheroList = new ArrayList<> | ||
+ | superheroList.add(new Superhero(" | ||
+ | superheroList.add(new Superhero(" | ||
+ | superheroList.add(new Superhero(" | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void onClick(View view) { | ||
+ | Superhero selectedCharacter = adapter.getSelectedSuperhero(); | ||
+ | if (selectedCharacter == null) | ||
+ | Toast.makeText(this, | ||
+ | |||
+ | Toast.makeText(this, | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | < | ||
+ | {{ recyclerview_sample.png }} | ||
+ | < | ||
+ | ===== ActionBar | ||
La '' | La '' | ||
Line 605: | Line 794: | ||
{{ youtube> | {{ youtube> | ||
- | === Menú contextual === | + | ===== Menú contextual |
En Android los menús contextuales aparecen cuando el usuario realiza una pulsación prolongada en alguna parte de la pantalla. Entonces se activa (si así se ha dispuesto) un menú asociado a dicho elemento que muestra opciones para realizar sobre dicho elemento. | En Android los menús contextuales aparecen cuando el usuario realiza una pulsación prolongada en alguna parte de la pantalla. Entonces se activa (si así se ha dispuesto) un menú asociado a dicho elemento que muestra opciones para realizar sobre dicho elemento. | ||
Line 697: | Line 886: | ||
{{ youtube> | {{ youtube> | ||
- | === Diálogos === | + | ===== Diálogos |
Los diálogos son ventanas emergentes que aparecen cuando el usuario debe seleccionar una acción antes de seguir con la ejecución de la aplicación. Se tratan siempre de ventana modales por lo que bloquean el flujo de ejecución de la aplicación hasta que el usuario selecciona qué hacer. | Los diálogos son ventanas emergentes que aparecen cuando el usuario debe seleccionar una acción antes de seguir con la ejecución de la aplicación. Se tratan siempre de ventana modales por lo que bloquean el flujo de ejecución de la aplicación hasta que el usuario selecciona qué hacer. | ||
Line 729: | Line 918: | ||
</ | </ | ||
- | ==== Mensajes emergentes ==== | + | ===== Mensajes emergentes |
Es posible mostrar pequeños mensajes emergentes de corta duración con la clase '' | Es posible mostrar pequeños mensajes emergentes de corta duración con la clase '' | ||
Line 747: | Line 936: | ||
</ | </ | ||
- | ==== Notificaciones ==== | + | ===== Notificaciones |
Las notificaciones son mensaje emergentes que aparecen en la pantalla de notificaciones del móvil. Sirve para mostrar información y también como forma rápida de acceso a la aplicación que emite dicha notificación, | Las notificaciones son mensaje emergentes que aparecen en la pantalla de notificaciones del móvil. Sirve para mostrar información y también como forma rápida de acceso a la aplicación que emite dicha notificación, | ||
Line 756: | Line 945: | ||
</ | </ | ||
- | En el siguiente ejemplo de código se puede ver como construir una Notificación con un título, un texto, un icono y una Activiy asociada a la que el usuario accederá si pulsa en la notificación | + | Lo primero que tendremos que hacer será definir |
<code java> | <code java> | ||
+ | private final String CHANNEL_ID = " | ||
. . . | . . . | ||
- | NotificationCompat.Builder nBuilder = new NotificationCompat.Builder(MainActivity.this) | + | private void createNotificationChannel() { |
+ | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||
+ | CharSequence name = " | ||
+ | String description = " | ||
+ | int importance = NotificationManager.IMPORTANCE_DEFAULT; | ||
+ | NotificationChannel channel = new NotificationChannel(CHANNEL_ID, | ||
+ | channel.setDescription(description); | ||
+ | channel.setImportance(NotificationManager.IMPORTANCE_HIGH); | ||
+ | NotificationManager notificationManager = getSystemService(NotificationManager.class); | ||
+ | notificationManager.createNotificationChannel(channel); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Y, a continuación, | ||
+ | |||
+ | <code java> | ||
+ | . . . | ||
+ | NotificationCompat.Builder nBuilder = new NotificationCompat.Builder(MainActivity.this, CHANNEL_ID) | ||
.setContentTitle(" | .setContentTitle(" | ||
.setContentText(" | .setContentText(" | ||
.setSmallIcon(R.drawable.default_marker) | .setSmallIcon(R.drawable.default_marker) | ||
- | .setContentIntent(PendingIntent.getActivity(MainActivity.this, | + | </ |
- | new Intent(MainActivity.this, | + | |
+ | Y lanzarla cuando sea necesario: | ||
+ | |||
+ | <code java> | ||
NotificationManager nManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); | NotificationManager nManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); | ||
nManager.notify(0, | nManager.notify(0, | ||
Line 772: | Line 982: | ||
</ | </ | ||
- | ((https://developer.android.com/guide/topics/ui/notifiers/notifications.html)) | + | === Asociar una Activity a una notificación === |
- | ===== Comunicación entre Activities ===== | + | |
+ | Se puede asociar una Activity a una notificación, | ||
+ | |||
+ | <code java> | ||
+ | // Definimos el Intent que permitirá lanzar la Activity | ||
+ | Intent intent = new Intent(this, OtraActivity.class); | ||
+ | PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); | ||
+ | . . . | ||
+ | // Vinculamos el Intent con la notificación | ||
+ | NotificationCompat.Builder builder = new NotificationCompat.Builder(this, | ||
+ | . . . | ||
+ | .setContentIntent(pendingIntent) | ||
+ | ... | ||
+ | </ | ||
+ | |||
+ | === Añadir acciones a una Notificación === | ||
+ | |||
+ | También podemos añadir diferentes acciones a una misma Notificación: | ||
+ | |||
+ | <code java> | ||
+ | // Podemos definir Intents si lo que queremos es lanzar diferentes Activities. En este caso añadiremos 2 acciones diferentes a una misma notificación: | ||
+ | . . . | ||
+ | // Y gestionar las diferentes acciones asociada a la notificación | ||
+ | NotificationCompat.Builder builder = new NotificationCompat.Builder(this, | ||
+ | . . . | ||
+ | .addAction(R.drawable.ic_hacer_algo, | ||
+ | .addAction(R.drawable.ic_otra_cosa, | ||
+ | . . . | ||
+ | </code> | ||
+ | |||
+ | < | ||
+ | {{ action-notification.png }} | ||
+ | </caption> | ||
+ | |||
+ | === Notificar el progreso en una notificación === | ||
+ | |||
+ | También podemos notificar el progreso de una tarea usando una notificación: | ||
+ | |||
+ | <code java> | ||
+ | NotificationCompat.Builder builder = new NotificationCompat.Builder(this, | ||
+ | . . . | ||
+ | .setContentText(" | ||
+ | .setProgress(100, | ||
+ | . . . | ||
+ | </ | ||
+ | |||
+ | |||
+ | < | ||
+ | {{ progress-notification.png }} | ||
+ | </ | ||
+ | |||
+ | Si lo que queremos es ir notificando el progresivo avance de una tarea, habría que meter la actualización del mismo ('' | ||
+ | ====== Comunicación entre Activities | ||
En algunas ocasiones necesitaremos pasar información entre dos // | En algunas ocasiones necesitaremos pasar información entre dos // | ||
Line 817: | Line 1079: | ||
{{ youtube> | {{ youtube> | ||
\\ | \\ | ||
- | ===== Gestión de permisos ===== | + | ====== Gestión de permisos |
En Android, para que las aplicaciones que instalamos en nuestro dispositivo, | En Android, para que las aplicaciones que instalamos en nuestro dispositivo, | ||
Line 871: | Line 1133: | ||
} | } | ||
</ | </ | ||
- | ===== Gestión de preferencias ===== | ||
- | Para la gestión | + | ====== Gestión |
- | <file xml preferencias.xml> | + | Para empezar tendremos que añadir una dependencia al fichero '' |
+ | |||
+ | <code groovy> | ||
+ | . . . | ||
+ | implementation ' | ||
+ | . . . | ||
+ | </ | ||
+ | |||
+ | Para la gestión de preferencias se debe diseñar un //layout// específico para crear la pantalla desde donde el usuario podrá configurar los diferentes aspectos que se preparen. Esta Activity será la que contenga el fragmento que será quien realmente tenga definidas las opciones que queramos que aparezcan: | ||
+ | |||
+ | <file xml activity_preferences.xml> | ||
+ | <?xml version=" | ||
+ | < | ||
+ | xmlns: | ||
+ | xmlns: | ||
+ | android: | ||
+ | android: | ||
+ | tools: | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | app: | ||
+ | app: | ||
+ | class=" | ||
+ | tools: | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | Este layout quedará relacionado con la Activity correspondiente donde se cargará como pantalla de Preferencias: | ||
+ | |||
+ | <file java PreferencesActivity.java> | ||
+ | public class PreferencesActivity extends AppCompatActivity { | ||
+ | |||
+ | @Override | ||
+ | protected void onCreate(Bundle savedInstanceState) { | ||
+ | super.onCreate(savedInstanceState); | ||
+ | setContentView(R.layout.activity_preferences); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | En '' | ||
+ | |||
+ | <file xml preference_screen.xml> | ||
+ | <?xml version=" | ||
< | < | ||
- | | + | |
- | < | + | |
- | <CheckBoxPreference | + | <SwitchPreferenceCompat |
- | | + | app:key="notifications" |
- | | + | app: |
- | android: | + | |
< | < | ||
- | android:key="opcion_nombre" | + | app:key="your_name" |
- | | + | app: |
- | | + | app: |
- | android: | + | |
- | </ | + | |
- | < | + | |
- | < | + | |
- | android: | + | |
- | android: | + | |
- | android: | + | |
- | android: | + | |
- | android: | + | |
- | android: | + | |
- | </ | + | |
</ | </ | ||
</ | </ | ||
+ | |||
+ | En cuando al código Java, tendremos que definir el fragmento que se encargará de cargar la pantalla de preferencias según el layout y las preferencias que hemos definido anteriormente. Hay que tener en cuenta que este fragmento quedará relacionado con el layout '' | ||
+ | |||
+ | <file java PreferenceFragment> | ||
+ | public class PreferencesFragment extends PreferenceFragmentCompat { | ||
+ | |||
+ | @Override | ||
+ | public void onCreatePreferences(Bundle savedInstanceState, | ||
+ | setPreferencesFromResource(R.xml.preference_screen, | ||
+ | } | ||
+ | } | ||
+ | |||
+ | </ | ||
+ | |||
+ | Hasta ahí hemos conseguido preparar la '' | ||
+ | |||
+ | <code java> | ||
+ | . . . | ||
+ | SharedPreferences myPreferences = PreferenceManager.getDefaultSharedPreferences(this); | ||
+ | boolean notifications = myPreferences.getBoolean(" | ||
+ | . . . | ||
+ | </ | ||
< | < | ||
Line 905: | Line 1226: | ||
< | < | ||
- | En el caso de que se usen '' | + | Hay que tener en cuenta que, en el caso de que se usen '' |
<file xml datos.xml> | <file xml datos.xml> | ||
Line 917: | Line 1238: | ||
</ | </ | ||
- | También existe un tipo de '' | + | ====== Imágenes ====== |
+ | |||
+ | En esta sección veremos una serie de artículos sobre cómo trabajar con imágenes en Android: hacer una foto, seleccionar una existente | ||
+ | |||
+ | ===== Hacer una foto ===== | ||
+ | |||
+ | El primer paso será indicar que la aplicación podrá solicitar los permisos para hacer uso de la cámara del dispositivo: | ||
+ | |||
+ | <code xml> | ||
+ | < | ||
+ | | ||
+ | | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | Además, tendremos que solicitar expresamente los permisos al inicio de la Activity | ||
<code java> | <code java> | ||
- | public class Preferencias | + | public class SomeActivity |
- | | + | . . . |
- | | + | |
- | | + | |
+ | . . . | ||
+ | protected | ||
+ | . . . | ||
+ | if (ActivityCompat.checkSelfPermission(this, | ||
+ | PackageManager.PERMISSION_DENIED) { | ||
+ | ActivityCompat.requestPermissions(this, | ||
+ | } | ||
+ | . . . | ||
+ | } | ||
+ | . . . | ||
+ | } | ||
+ | . . . | ||
+ | </ | ||
- | addPreferencesFromResource(R.layout.preferencias); | + | Justo en el momento en que el usuario pincha en un botón o un '' |
+ | |||
+ | <code java> | ||
+ | . . . | ||
+ | if (ActivityCompat.checkSelfPermission(this, | ||
+ | PackageManager.PERMISSION_GRANTED) { | ||
+ | launchCamera(); | ||
+ | } | ||
+ | . . . | ||
+ | </ | ||
+ | |||
+ | El proceso tiene 2 pasos: | ||
+ | - '' | ||
+ | - El objeto '' | ||
+ | |||
+ | A partir de aqui, si lo que queremos hacer es almacenarla en base de datos, podemos hacerlo siguiendo los pasos que se indican en [[https:// | ||
+ | |||
+ | <code java> | ||
+ | ActivityResultLauncher< | ||
+ | new ActivityResultContracts.StartActivityForResult(), | ||
+ | new ActivityResultCallback< | ||
+ | @Override | ||
+ | public void onActivityResult(ActivityResult result) { | ||
+ | if (result.getResultCode() == RESULT_OK) { | ||
+ | // Podemos mostrar la miniatura en un ImageView | ||
+ | imageView.setImageURI(pictureUri); | ||
+ | // Podemos tener la imagen como Bitmap si queremos guardarla en base de datos, por ejemplo | ||
+ | Bitmap bitmapImage = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); | ||
+ | } | ||
+ | } | ||
+ | }); | ||
+ | |||
+ | private void launchCamera() { | ||
+ | ContentValues values = new ContentValues(); | ||
+ | values.put(MediaStore.Images.Media.TITLE, | ||
+ | values.put(MediaStore.Images.Media.DESCRIPTION, | ||
+ | pictureUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, | ||
+ | Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); | ||
+ | cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, | ||
+ | |||
+ | startCamera.launch(cameraIntent); | ||
+ | } | ||
+ | </ | ||
+ | ===== Seleccionar una imagen de la galería del móvil ===== | ||
+ | |||
+ | En el siguiente ejemplo suponemos que tenemos un '' | ||
+ | |||
+ | <code java> | ||
+ | public class RegisterActivity extends AppCompatActivity { | ||
+ | . . . | ||
+ | private ImageView imageView; | ||
+ | |||
+ | public void onCreate(.....) { | ||
+ | . . . | ||
+ | imageView = findViewById(R.id.myImageView); | ||
+ | . . . | ||
} | } | ||
+ | | ||
+ | ActivityResultLauncher< | ||
+ | new ActivityResultContracts.StartActivityForResult(), | ||
+ | result -> { | ||
+ | if (result.getResultCode() == Activity.RESULT_OK) { | ||
+ | Uri image_uri = result.getData().getData(); | ||
+ | imageView.setImageURI(image_uri); | ||
+ | } | ||
+ | } | ||
+ | ); | ||
+ | |||
+ | public void selectImage(View view) { | ||
+ | Intent galleryIntent = new Intent(Intent.ACTION_PICK, | ||
+ | galleryActivityResultLauncher.launch(galleryIntent); | ||
+ | } | ||
+ | . . . | ||
} | } | ||
</ | </ | ||
- | Hasta ahí hemos conseguido preparar | + | Más adelante, cuando queramos guardar |
+ | |||
+ | <code java> | ||
+ | Bitmap bitmapImage = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); | ||
+ | </ | ||
+ | |||
+ | Y ese objeto // | ||
+ | |||
+ | ===== Almacenar una imagen | ||
+ | |||
+ | Una forma sencilla | ||
+ | El campo que almacenará la imagen será, por tanto, un '' | ||
<code java> | <code java> | ||
+ | public class Book { | ||
. . . | . . . | ||
- | SharedPreferences preferencias = PreferenceManager.getDefaultSharedPreferences(this); | + | @ColumnInfo |
- | boolean verFavoritos = preferencias.getBoolean(" | + | |
. . . | . . . | ||
+ | } | ||
</ | </ | ||
+ | En el momento en que cogemos la Uri de la foto para luego ver su vista previa en un ImageView, podemos también convertir ese valor a un String y asignarlo al campo que hemos definido en el momento de dar de alta, en este caso, un libro: | ||
+ | <code java> | ||
+ | . . . | ||
+ | if (result.getResultCode() == Activity.RESULT_OK) { | ||
+ | Uri uri = result.getData().getData(); | ||
+ | bookImage.setImageURI(uri); | ||
+ | imageUri = uri.toString(); | ||
+ | } | ||
+ | . . . | ||
+ | </ | ||
- | ===== Acceso a Bases de Datos ===== | + | Asi, al crear el objeto justo antes de pasarlo al Dao de //Room//, podemos asignarle el valor: |
+ | |||
+ | <code java> | ||
+ | book.setImage(imageUri); | ||
+ | </ | ||
+ | |||
+ | Y a la hora de cargarlo (por ejemplo en el //Adapter// del '' | ||
+ | |||
+ | <code java> | ||
+ | . . . | ||
+ | if (book.getImage() != null) { | ||
+ | holder.ivImage.setImageURI(Uri.parse(book.getImage())); | ||
+ | } | ||
+ | . . . | ||
+ | </ | ||
+ | ====== Acceso a Bases de Datos con SQLite [deprecated] ====== | ||
El acceso a Bases de Datos está integrado con el API de //Android// por lo que resulta muy sencillo. Además, como ya se ha visto en la estructura de este framework, se incluye con el mismo el motor de almacenamiento //SQLite//, que será el que se emplee para almacenar información dentro del dispositivo móvil. Si se quiere almacenar información fuera del teléfono (que tendrá que ser accedida vía Servicio Web, por ejemplo) se podrán utilizar otros motores puesto que son otros equipos quienes tendrán que gestionar el acceso a los datos. | El acceso a Bases de Datos está integrado con el API de //Android// por lo que resulta muy sencillo. Además, como ya se ha visto en la estructura de este framework, se incluye con el mismo el motor de almacenamiento //SQLite//, que será el que se emplee para almacenar información dentro del dispositivo móvil. Si se quiere almacenar información fuera del teléfono (que tendrá que ser accedida vía Servicio Web, por ejemplo) se podrán utilizar otros motores puesto que son otros equipos quienes tendrán que gestionar el acceso a los datos. | ||
Line 953: | Line 1411: | ||
* Se puede utilizar '' | * Se puede utilizar '' | ||
- | === Acceso a Bases de Datos === | + | === Acceso a Bases de Datos con SQLiteHelper (deprecated) |
En // | En // | ||
Line 1147: | Line 1605: | ||
</ | </ | ||
- | ==== Almacenar imágenes en Base de Datos ==== | + | === Almacenar imágenes en Base de Datos (con SQLite) [deprecated] |
Merece especial atención este caso, puesto que almacenar imágenes en una Base de Datos SQLite no es una tarea trivial. | Merece especial atención este caso, puesto que almacenar imágenes en una Base de Datos SQLite no es una tarea trivial. | ||
Line 1155: | Line 1613: | ||
En este caso, podríamos utilizar una vista '' | En este caso, podríamos utilizar una vista '' | ||
+ | |||
+ | Usaremos la librería [[https:// | ||
< | < | ||
Line 1164: | Line 1624: | ||
@Override | @Override | ||
protected void onActivityResult(int requestCode, | protected void onActivityResult(int requestCode, | ||
- | + | super(requestCode, | |
- | if ((requestCode == RESULTADO_CARGA_IMAGEN) && (resultCode == RESULT_OK) | + | if ((requestCode == RESULTADO_CARGA_IMAGEN) && (resultCode == RESULT_OK) |
- | | + | && (data != null)) { |
- | // Obtiene el Uri de la imagen seleccionada por el usuario | + | |
- | Uri imagenSeleccionada = data.getData(); | + | .into((ImageView) findViewById(R.id.imageView)); |
- | String[] ruta = {MediaStore.Images.Media.DATA }; | + | } |
- | + | ||
- | // Realiza una consulta a la galería de imágenes solicitando la imagen seleccionada | + | |
- | Cursor cursor = getContentResolver().query(imagenSeleccionada, | + | |
- | cursor.moveToFirst(); | + | |
- | + | ||
- | // Obtiene la ruta a la imagen | + | |
- | int indice = cursor.getColumnIndex(ruta[0]); | + | |
- | String picturePath = cursor.getString(indice); | + | |
- | | + | |
- | + | ||
- | // Carga la imagen en una vista ImageView que se encuentra en | + | |
- | // en layout de la Activity actual | + | |
- | ImageView imageView = (ImageView) findViewById(R.id.ivImagen); | + | |
- | | + | |
- | } | + | |
} | } | ||
. . . | . . . | ||
Line 1309: | Line 1754: | ||
} | } | ||
</ | </ | ||
- | ===== Acceder a la red ===== | ||
- | Uno de los mayores atractivos de los dispositivos móviles es el acceso a Internet para recuperar y tratar información de cualquier tipo. Prácticamente todas las aplicaciones permiten comunicar los dispositivos entre sí o con Internet. Para ello, siempre tendremos que contar con una aplicación servidor que permita la comunicación entre los diferentes dispositivos. Dicho servidor, normalmente, | + | |
+ | ====== Acceso a Bases de Datos con Room ====== | ||
+ | |||
+ | === Room persistence library === | ||
+ | |||
+ | * Librería incluida con Android que abstrae todavía más al usuario de conocer los detalles de la base de datos | ||
+ | * Según se define la clase Java es posible añadir anotaciones para indicar cómo debe almacenar dicho objeto cuando se quiera llevar a base de datos | ||
+ | |||
+ | < | ||
+ | {{ room.png }} | ||
+ | < | ||
+ | |||
+ | En primer lugar, necesitamos añadir las dependencias necesarias al fichero '' | ||
+ | |||
+ | <code groovy> | ||
+ | . . . | ||
+ | implementation " | ||
+ | annotationProcessor " | ||
+ | . . . | ||
+ | </ | ||
+ | |||
+ | Asi, para una clase '' | ||
+ | |||
+ | <code java> | ||
+ | @Entity | ||
+ | public class Product { | ||
+ | |||
+ | @PrimaryKey(autoGenerate = true) | ||
+ | private int id; | ||
+ | @ColumnInfo | ||
+ | private String name; | ||
+ | @ColumnInfo | ||
+ | private String category; | ||
+ | @ColumnInfo | ||
+ | private int quantity; | ||
+ | @ColumnInfo | ||
+ | private float price; | ||
+ | @ColumnInfo | ||
+ | private boolean important; | ||
+ | @ColumnInfo(typeAffinity = ColumnInfo.BLOB) | ||
+ | private byte[] image; | ||
+ | . . . | ||
+ | . . . | ||
+ | </ | ||
+ | |||
+ | A continuación definiremos el //DAO// con todas las operaciones que queramos que estén disponibles (Habría que hacer un //DAO// para cada clase del modelo de datos: | ||
+ | |||
+ | <code java> | ||
+ | @Dao | ||
+ | public interface ProductDao { | ||
+ | @Query(" | ||
+ | List< | ||
+ | |||
+ | @Query(" | ||
+ | List< | ||
+ | |||
+ | @Insert | ||
+ | void insert(Product product); | ||
+ | |||
+ | @Update | ||
+ | void update(Product product); | ||
+ | |||
+ | @Delete | ||
+ | void delete(Product product); | ||
+ | |||
+ | @Query(" | ||
+ | void deleteByName(String name); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Y, por último, la clase que nos dará acceso a la Base de datos allá donde la queramos utilizar para realizar alguna operación sobre ella: | ||
+ | |||
+ | <code java> | ||
+ | @Database(entities = {Product.class}, | ||
+ | public abstract class AppDatabase extends RoomDatabase { | ||
+ | public abstract ProductDao productDao(); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | === Registrar/ | ||
+ | |||
+ | <code java> | ||
+ | // Se recogen los datos para construir el nuevo Producto | ||
+ | Product product = . . . | ||
+ | . . . | ||
+ | final AppDatabase db = Room.databaseBuilder(this, | ||
+ | .allowMainThreadQueries().build(); | ||
+ | try { | ||
+ | db.productDao().insert(product); | ||
+ | . . . | ||
+ | } catch (SQLiteConstraintException sce) { | ||
+ | Snackbar.make(aView, | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | === Consultas === | ||
+ | |||
+ | Podríamos necesitar listar todos los productos de la base de datos: | ||
+ | |||
+ | <code java> | ||
+ | AppDatabase db = AppDatabase db = Room.databaseBuilder(context, | ||
+ | | ||
+ | | ||
+ | List< | ||
+ | </ | ||
+ | |||
+ | O bien hacer alguna de las consultas que hemos definido en el DAO: | ||
+ | |||
+ | <code java> | ||
+ | // Recogemos el nombre del producto por el que queremos filtrar | ||
+ | String productName = . . . | ||
+ | AppDatabase db = AppDatabase db = Room.databaseBuilder(context, | ||
+ | | ||
+ | | ||
+ | Product product = db.productDao().findByName(productName); | ||
+ | </ | ||
+ | |||
+ | === Eliminar información === | ||
+ | |||
+ | <code java> | ||
+ | // Nos hacemos con el producto que queremos eliminar | ||
+ | Book selectedBook = . . . | ||
+ | . . . | ||
+ | final AppDatabase db = Room.databaseBuilder(this, | ||
+ | .allowMainThreadQueries().build(); | ||
+ | try { | ||
+ | db.productDao().delete(product); | ||
+ | . . . | ||
+ | } catch (SQLiteConstraintException sce) { | ||
+ | Snackbar.make(aView, | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | ====== Consumo de APIs ====== | ||
+ | |||
+ | Uno de los mayores atractivos de los dispositivos móviles es el acceso a Internet para recuperar y tratar información de cualquier tipo. Prácticamente todas las aplicaciones permiten comunicar los dispositivos entre sí o con Internet. Para ello, siempre tendremos que contar con una aplicación servidor que permita la comunicación entre los diferentes dispositivos. Dicho servidor, normalmente, | ||
< | < | ||
Line 1317: | Line 1898: | ||
< | < | ||
- | + | ===== Formato JSON ===== | |
- | ==== Formato JSON ==== | + | |
<code java> | <code java> | ||
Line 1350: | Line 1930: | ||
} | } | ||
</ | </ | ||
- | ==== Tareas asíncronas: | + | |
+ | ===== Consumo de APIs con Retrofit ===== | ||
+ | |||
+ | Para hacer este ejemplo, de consumo de APIs usando Retrofit, vamos a suponer: | ||
+ | * Tenemos una API de productos funcionando en local con las siguientes operaciones: | ||
+ | * POST /products | ||
+ | * GET / | ||
+ | * DELETE / | ||
+ | * GET /products | ||
+ | * Tenemos la clase Java definida con sus atributos, getters y setters | ||
+ | * Estamos usando una arquitectura MVP (Model-View-Presenter) | ||
+ | * Solamente vamos a montar el ejemplo que corresponde con el registro de un nuevo Producto | ||
+ | |||
+ | Para empezar, añadimos las dependencias que necesitamos al fichero '' | ||
+ | |||
+ | <code groovy> | ||
+ | implementation ' | ||
+ | implementation ' | ||
+ | </ | ||
+ | |||
+ | Y también tendremos que crear las clases e interfaces que nos permiten " | ||
+ | |||
+ | Primero la clase que nos permitirá crear una instancia de nuestra API en este proyecto, "en el lado Android" | ||
+ | |||
+ | <code java> | ||
+ | public class ProductApi { | ||
+ | |||
+ | public static ProductApiInterface buildInstance() { | ||
+ | Retrofit retrofit = new Retrofit.Builder() | ||
+ | .baseUrl(BASE_URL) | ||
+ | .addConverterFactory(GsonConverterFactory.create()) | ||
+ | .build(); | ||
+ | return retrofit.create(ProductApiInterface.class); | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | A continuación la interface con las operaciones de la API que queremos que Retrofit implemente para nosotros: | ||
+ | |||
+ | <code java> | ||
+ | public interface ProductApiInterface { | ||
+ | |||
+ | Call< | ||
+ | . . . | ||
+ | . . . | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | A continuación la interface que define el contrato para nuestra Arquitectura MVP. Hay que tener en cuenta que para este ejemplo solamente hemos dejado el código que corresponde a la operación de registrar un producto: | ||
+ | |||
+ | <file java NewProductContract.java> | ||
+ | public interface NewProductContract { | ||
+ | interface Model { | ||
+ | interface OnAddProductListener { | ||
+ | void onAddProductSuccess(Product newProduct); | ||
+ | void onAddProductError(String message); | ||
+ | } | ||
+ | void addProduct(Product product, OnAddProductListener listener); | ||
+ | . . . | ||
+ | } | ||
+ | |||
+ | interface View { | ||
+ | void addProduct(android.view.View view); | ||
+ | . . . | ||
+ | } | ||
+ | |||
+ | interface Presenter { | ||
+ | void addProduct(String name, String category, String quantity, String price, boolean important, byte[] productImage); | ||
+ | . . . | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | El modelo es la capa que contiene las operaciones donde nuestra aplicación Android se comunica con la API del lado servidor. En función de como se ejecuten esas operaciones, | ||
+ | |||
+ | <file java NewProductModel.java> | ||
+ | public class NewProductModel implements NewProductContract.Model { | ||
+ | |||
+ | private Context context; | ||
+ | |||
+ | public NewProductModel(Context context) { | ||
+ | this.context = context; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void addProduct(Product product, OnAddProductListener listener) { | ||
+ | ProductApiInterface api = ProductApi.buildInstance(); | ||
+ | Call< | ||
+ | callProducts.enqueue(new Callback< | ||
+ | @Override | ||
+ | public void onResponse(Call< | ||
+ | Product product = response.body(); | ||
+ | listener.onAddProductSuccess(product); | ||
+ | } | ||
+ | @Override | ||
+ | public void onFailure(Call< | ||
+ | listener.onAddProductError(" | ||
+ | t.printStackTrace(); | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | . . . | ||
+ | . . . | ||
+ | </ | ||
+ | |||
+ | El Presenter, como capa que hacer de " | ||
+ | |||
+ | <file java NewProductPresenter.java> | ||
+ | public class NewProductPresenter implements NewProductContract.Presenter, | ||
+ | NewProductContract.Model.OnAddProductListener, | ||
+ | |||
+ | private NewProductModel model; | ||
+ | private NewProductView view; | ||
+ | |||
+ | public NewProductPresenter(NewProductView view) { | ||
+ | this.view = view; | ||
+ | model = new NewProductModel(view.getApplicationContext()); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void addProduct(String name, String category, String quantity, String price, boolean important, byte[] productImage) { | ||
+ | if (!validData(name, | ||
+ | view.showMessage(" | ||
+ | |||
+ | Product product = new Product(name, | ||
+ | Float.parseFloat(price), | ||
+ | model.addProduct(product, | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void onAddProductSuccess(Product product) { | ||
+ | view.showMessage(" | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void onAddProductError(String message) { | ||
+ | view.showMessage(" | ||
+ | } | ||
+ | . . . | ||
+ | . . . | ||
+ | </ | ||
+ | |||
+ | Para este caso, en esta capa, el usuario ve el formulario con el que podrá registrar un nuevo producto. También se implementan aqui los métodos a los que el Presenter tiene que invocar cuando todo vaya bien o mal al intentar registrar un producto: | ||
+ | |||
+ | <file java NewProductView.java> | ||
+ | public class NewProductView extends AppCompatActivity implements NewProductContract.View { | ||
+ | |||
+ | private int SELECT_PICTURE_RESULT = 1; | ||
+ | private NewProductPresenter presenter; | ||
+ | private Action action; | ||
+ | private Product product; | ||
+ | |||
+ | @Override | ||
+ | protected void onCreate(Bundle savedInstanceState) { | ||
+ | super.onCreate(savedInstanceState); | ||
+ | setContentView(R.layout.activity_new_product); | ||
+ | |||
+ | // Tenemos una enumeración definida para saber si tenemos que Registrar o Modificar un producto | ||
+ | // Eso nos permite reutilizar el formulario que usamos para la recogida de datos | ||
+ | action = Action.valueOf(getIntent().getStringExtra(" | ||
+ | if (action == PUT) { | ||
+ | product = getIntent().getParcelableExtra(" | ||
+ | fillProductDetails(); | ||
+ | } | ||
+ | |||
+ | presenter = new NewProductPresenter(this); | ||
+ | } | ||
+ | |||
+ | public void addProduct(View view) { | ||
+ | EditText etName = findViewById(R.id.product_name); | ||
+ | EditText etCategory = findViewById(R.id.product_category); | ||
+ | EditText etQuantity = findViewById(R.id.product_quantity); | ||
+ | EditText etPrice = findViewById(R.id.product_price); | ||
+ | CheckBox checkImportant = findViewById(R.id.important_product); | ||
+ | ImageView productImageView = findViewById(R.id.product_image); | ||
+ | |||
+ | String name = etName.getText().toString(); | ||
+ | String category = etCategory.getText().toString(); | ||
+ | String quantity = etQuantity.getText().toString(); | ||
+ | String price = etPrice.getText().toString(); | ||
+ | boolean important = checkImportant.isChecked(); | ||
+ | byte[] productImage = ImageUtils.fromImageViewToByteArray(productImageView); | ||
+ | |||
+ | if (action == POST) | ||
+ | presenter.addProduct(name, | ||
+ | else | ||
+ | presenter.modifyProduct(product.getId(), | ||
+ | |||
+ | etName.setText("" | ||
+ | etCategory.setText("" | ||
+ | etQuantity.setText("" | ||
+ | etPrice.setText("" | ||
+ | checkImportant.setChecked(false); | ||
+ | etName.requestFocus(); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void showMessage(String message) { | ||
+ | Toast.makeText(this, | ||
+ | } | ||
+ | |||
+ | public void selectPicture(View view) { | ||
+ | Intent intent = new Intent(Intent.ACTION_PICK, | ||
+ | startActivityForResult(intent, | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | protected void onActivityResult(int requestCode, | ||
+ | super.onActivityResult(requestCode, | ||
+ | if ((requestCode == SELECT_PICTURE_RESULT) && (resultCode == RESULT_OK) | ||
+ | && | ||
+ | | ||
+ | .noPlaceholder() | ||
+ | .centerCrop() | ||
+ | .fit() | ||
+ | .into((ImageView) findViewById(R.id.product_image)); | ||
+ | } | ||
+ | } | ||
+ | . . . | ||
+ | . . . | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===== Tareas asíncronas: | ||
Las tareas asíncronas ('' | Las tareas asíncronas ('' | ||
Line 1404: | Line 2207: | ||
</ | </ | ||
- | ==== Acceder a contenido en la red ==== | + | |
+ | ===== Acceder a contenido en la red [deprecated] ===== | ||
En el caso concreto de que queramos acceder a contenido en la red es muy probable que éste nos venga preparado en formato //JSON// como hemos visto anteriormente. Además, hay que tener en cuenta que //Android// nos obliga a implementar en un // | En el caso concreto de que queramos acceder a contenido en la red es muy probable que éste nos venga preparado en formato //JSON// como hemos visto anteriormente. Además, hay que tener en cuenta que //Android// nos obliga a implementar en un // | ||
Line 1585: | Line 2389: | ||
. . . | . . . | ||
</ | </ | ||
- | ===== Mapas y ubicaciones ===== | ||
- | ==== Utilizar el mapa de Google Maps ==== | + | ====== Google Maps ====== |
+ | |||
+ | Antes de poder trabajar con la API de Google Maps en cualquier proyecto de Android, tendremos que configurar nuestra cuenta en '' | ||
+ | |||
+ | Una vez tengamos el proyecto registrado en la Google Cloud Console y hayamos solicitado la API Key asociada a la API de Google Maps, guardaremos ésta en nuestro proyecto en res-> | ||
+ | |||
+ | <code xml> | ||
+ | < | ||
+ | <string name=" | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | Y tenemos que asegurarnos de que indicamos la ubicación de esta API Key en el manifest de nuestro proyecto en Android Studio (podemos colocarlo justo donde termina la definición de todas las Activities): | ||
+ | |||
+ | <code xml> | ||
+ | . . . | ||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | . . . | ||
+ | </ | ||
+ | |||
+ | ===== Utilizar el mapa de Google Maps ===== | ||
< | < | ||
Line 1596: | Line 2421: | ||
<file xml activity_mapa.xml> | <file xml activity_mapa.xml> | ||
- | <fragment | + | <androidx.constraintlayout.widget.ConstraintLayout |
- | | + | |
android: | android: | ||
android: | android: | ||
- | | + | |
+ | |||
+ | < | ||
+ | xmlns: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | tools: | ||
+ | |||
+ | </ | ||
</ | </ | ||
- | Más adelante, en la '' | + | Más adelante, en la '' |
+ | |||
+ | Asi, en el método '' | ||
<code java> | <code java> | ||
- | public class Mapa extends | + | public class MapsActivity |
- | private GoogleMap | + | private GoogleMap |
- | + | ||
- | private double latitud; | + | |
- | private double longitud; | + | |
- | private String nombre; | + | |
@Override | @Override | ||
Line 1619: | Line 2452: | ||
setContentView(R.layout.activity_mapa); | setContentView(R.layout.activity_mapa); | ||
- | | + | |
- | try { | + | |
- | | + | |
- | } catch (Exception e) { | + | } |
- | | + | |
- | } | + | @Override |
+ | public void onMapReady(GoogleMap googleMap) { | ||
+ | | ||
+ | map.getUiSettings().setZoomControlsEnabled(true); | ||
+ | | ||
+ | | ||
+ | } | ||
+ | </ | ||
- | // Obtiene | + | ===== Seleccionar ubicaciones utilizando un mapa Mapbox ===== |
- | mapa = ((MapFragment) getFragmentManager().findFragmentById(R.id.map)).getMap(); | + | |
+ | Una vez que ya tenemos lo necesario para visualizar el mapa siguiendo los pasos del punto anterior, vamos a ver cómo empezar a interactuar con él. | ||
+ | |||
+ | Como primer ejemplo veremos cómo seleccionar | ||
+ | |||
+ | Para ello, necesitaremos los componentes '' | ||
+ | |||
+ | * El método '' | ||
+ | * El método '' | ||
+ | * El método '' | ||
+ | * El método '' | ||
+ | * El método '' | ||
+ | * El método '' | ||
+ | |||
+ | <code java> | ||
+ | public class MapActivity extends AppCompatActivity implements Style.OnStyleLoaded, | ||
+ | |||
+ | private MapView mapView; | ||
+ | private PointAnnotationManager pointAnnotationManager; | ||
+ | private GesturesPlugin gesturesPlugin; | ||
+ | private Point currentPoint; | ||
+ | |||
+ | @Override | ||
+ | protected void onCreate(Bundle savedInstanceState) { | ||
+ | | ||
+ | initializeMapView(); | ||
+ | initializePointAnnotationManager(); | ||
+ | initializeGesturesPlugin(); | ||
+ | . . . | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void onStyleLoaded(@NonNull Style style) { | ||
+ | |||
+ | } | ||
+ | |||
+ | private void initializeMapView() { | ||
+ | mapView = findViewById(R.id.mapView); | ||
+ | mapView.getMapboxMap().loadStyleUri(Style.MAPBOX_STREETS, | ||
+ | } | ||
+ | |||
+ | private void initializePointAnnotationManager() { | ||
+ | AnnotationPlugin annotationPlugin = AnnotationPluginImplKt.getAnnotations(mapView); | ||
+ | AnnotationConfig annotationConfig = new AnnotationConfig(); | ||
+ | pointAnnotationManager = PointAnnotationManagerKt.createPointAnnotationManager(annotationPlugin, | ||
+ | } | ||
+ | |||
+ | private void initializeGesturesPlugin() { | ||
+ | gesturesPlugin = GesturesUtils.getGestures(mapView); | ||
+ | gesturesPlugin.addOnMapClickListener(this); | ||
+ | } | ||
+ | |||
+ | private void addMarker(double latitude, double longitude, String title) { | ||
+ | PointAnnotationOptions pointAnnotationOptions = new PointAnnotationOptions() | ||
+ | | ||
+ | | ||
+ | | ||
+ | pointAnnotationManager.create(pointAnnotationOptions); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public boolean onMapClick(@NonNull Point point) { | ||
+ | pointAnnotationManager.deleteAll(); | ||
+ | currentPoint = point; | ||
+ | addMarker(point.latitude(), | ||
+ | return false; | ||
} | } | ||
} | } | ||
</ | </ | ||
- | ==== Marcar ubicaciones en el mapa de Google Maps ==== | + | ===== Marcar ubicaciones en el mapa de Google Maps ===== |
Para los ejemplos de estos apuntes se han utilizado datos geolocalizados del [[http:// | Para los ejemplos de estos apuntes se han utilizado datos geolocalizados del [[http:// | ||
Line 1718: | Line 2623: | ||
{{ youtube> | {{ youtube> | ||
- | ==== Utilizar los mapas con la librería | + | ====== Mapbox |
< | < | ||
Line 1725: | Line 2630: | ||
Mapbox es [[http:// | Mapbox es [[http:// | ||
- | Lo primero que tenemos que hacer, para poder utilizar la librería //Mapbox// es añadir, al fichero '' | ||
- | <code java> | + | El primer paso es seguir la [[https:// |
- | repositories | + | |
- | | + | En la gúia, una vez realizados los pasos para registrarnos, |
- | } | + | |
- | dependencies | + | También hay bastantes [[https:// |
- | . . . | + | |
- | | + | ===== Configurar el proyecto para comenzar a usar Mapbox ===== |
- | | + | |
+ | * Una vez que nos hemos registrado en https:// | ||
+ | |||
+ | <figure> | ||
+ | {{ create-token-mapbox.png }} | ||
+ | < | ||
+ | |||
+ | < | ||
+ | {{ token-mapbox.png }} | ||
+ | < | ||
+ | |||
+ | | ||
+ | |||
+ | <code javascript> | ||
+ | MAPBOX_DOWNLOADS_TOKEN=sk.eyJ1Ijoic2ZhY2kiLCkjahsdjkhJHJHMGFzZTJpazFqZGd6ZGVxYiJ9.jBfUMuKJHjhjhjh3nMg | ||
+ | </ | ||
+ | |||
+ | * En el fichero '' | ||
+ | |||
+ | <code kotlin> | ||
+ | . . . | ||
+ | dependencyResolutionManagement | ||
+ | | ||
+ | | ||
+ | google() | ||
+ | mavenCentral() | ||
+ | |||
+ | maven { | ||
+ | url = uri(" | ||
+ | authentication { | ||
+ | create< | ||
+ | } | ||
+ | credentials { | ||
+ | // Do not change the username below. | ||
+ | // This should always be `mapbox` (not your username). | ||
+ | username = "mapbox" | ||
+ | // Use the secret token you stored in gradle.properties as the password | ||
+ | password = "sk.eyJ1Ijoic2ZhY2kiLCkjahsdjkhJHJHMGFzZTJpazFqZGd6ZGVxYiJ9.jBfUMuKJHjhjhjh3nMg" | ||
+ | } | ||
+ | | ||
} | } | ||
} | } | ||
+ | . . . | ||
</ | </ | ||
- | El siguiente paso, en el layout de la Activity donde queramos | + | * En el fichero '' |
- | <file xml activity_main.xml> | + | <code xml> |
+ | . . . | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | |||
+ | < | ||
+ | . . . | ||
+ | . . . | ||
+ | </ | ||
+ | |||
+ | * También tendremos que crear un fichero '' | ||
+ | |||
+ | < | ||
<?xml version=" | <?xml version=" | ||
- | <RelativeLayout xmlns: | + | <resources> |
- | | + | <string name="mapbox_access_token" |
- | | + | </resources> |
- | android: | + | </code> |
- | xmlns:mapbox=" | + | |
- | | + | Y ahora es el momento de comenzar ya a trabajar con nuestra aplicación, |
- | android:paddingLeft="@dimen/ | + | |
- | android:paddingRight="@dimen/ | + | En el layout de la Activity donde queramos que aparezca el mapa, tendremos que añadir el código XML que permite insertar el componente donde se dibujará el mapa. En este caso, hemos dejado predefinidas las coordenadas de la ciudad de Zaragoza y un nivel de zoom de 12. En cualquier caso son parámetros que luego desde el código pueden ser modificados e incluso el usuario haciendo uso de la pantalla podrá modificar a su gusto. |
- | android:paddingTop="@dimen/ | + | |
- | tools: | + | <file xml activity_maps.xml> |
+ | <?xml version="1.0" | ||
+ | < | ||
+ | android:layout_width="match_parent" | ||
+ | android:layout_height="match_parent" | ||
+ | android:orientation="vertical"> | ||
- | | + | |
- | android: | + | xmlns: |
- | android: | + | xmlns: |
- | android: | + | android: |
- | mapbox:center_latitude="41.656287" | + | android: |
- | mapbox:center_longitude="-0.876538" | + | android: |
- | mapbox:zoom="12" | + | mapbox:mapbox_cameraTargetLat="40.7128" |
- | | + | mapbox:mapbox_cameraTargetLng="-74.0060" |
- | </com.mapbox.mapboxsdk.maps.MapView> | + | mapbox:mapbox_cameraZoom="9.0" |
- | </ | + | /> |
+ | </androidx.constraintlayout.widget.ConstraintLayout> | ||
</ | </ | ||
- | En cuanto al código Java, necesitaremos inicializar la librería con la llamada | + | En cuanto al ćodigo, será necesario acceder al objeto |
<file java MainActivity.java> | <file java MainActivity.java> | ||
- | public class MainActivity | + | public class MapsActivity |
- | | + | private |
- | | + | |
- | protected void onCreate(Bundle savedInstanceState) { | + | protected void onCreate(Bundle savedInstanceState) { |
- | super.onCreate(savedInstanceState); | + | super.onCreate(savedInstanceState); |
+ | setContentView(R.layout.activity_maps); | ||
- | MapboxAccountManager.start(this, | + | mapView = findViewById(R.id.mapView); |
- | + | | |
- | setContentView(R.layout.activity_main); | + | } |
- | | + | |
- | | + | |
- | | + | |
} | } | ||
</ | </ | ||
- | Y, por último, antes de poder lanzar la aplicación, | + | < |
- | Además, tenemos que habilitar el servicio de telemetría de la librería //Mapbox//. | + | {{ map-view.png }} |
- | Todos estos cambios se deben realizar en el fichero '' | + | < |
+ | ===== Seleccionar ubicaciones utilizando un mapa Mapbox ===== | ||
- | <code xml> | + | Una vez que ya tenemos lo necesario para visualizar el mapa siguiendo los pasos del punto anterior, vamos a ver cómo empezar a interactuar con él. |
- | . . . | + | |
- | < | + | Como primer ejemplo veremos cómo seleccionar una ubicación en el mapa haciendo click en ella y colocando un " |
- | . . . | + | |
- | | + | Para ello, necesitaremos los componentes '' |
- | < | + | |
- | | + | |
- | | + | |
- | <uses-permission android: | + | * El método '' |
- | . . . | + | |
- | | + | * El método '' |
+ | * El método '' | ||
+ | |||
+ | <code java> | ||
+ | public class MapActivity extends AppCompatActivity implements Style.OnStyleLoaded, | ||
+ | |||
+ | | ||
+ | private PointAnnotationManager pointAnnotationManager; | ||
+ | private GesturesPlugin gesturesPlugin; | ||
+ | private Point currentPoint; | ||
+ | |||
+ | @Override | ||
+ | protected void onCreate(Bundle savedInstanceState) { | ||
. . . | . . . | ||
- | | + | |
+ | initializePointAnnotationManager(); | ||
+ | initializeGesturesPlugin(); | ||
. . . | . . . | ||
- | | + | |
- | . . . | + | |
- | </ | + | @Override |
- | . . . | + | public void onStyleLoaded(@NonNull Style style) { |
+ | |||
+ | } | ||
+ | |||
+ | private void initializeMapView() { | ||
+ | mapView = findViewById(R.id.mapView); | ||
+ | mapView.getMapboxMap().loadStyleUri(Style.MAPBOX_STREETS, | ||
+ | } | ||
+ | |||
+ | private void initializePointAnnotationManager() { | ||
+ | AnnotationPlugin annotationPlugin = AnnotationPluginImplKt.getAnnotations(mapView); | ||
+ | AnnotationConfig annotationConfig = new AnnotationConfig(); | ||
+ | pointAnnotationManager = PointAnnotationManagerKt.createPointAnnotationManager(annotationPlugin, | ||
+ | } | ||
+ | |||
+ | private void initializeGesturesPlugin() { | ||
+ | gesturesPlugin = GesturesUtils.getGestures(mapView); | ||
+ | gesturesPlugin.addOnMapClickListener(this); | ||
+ | } | ||
+ | |||
+ | private void addMarker(double latitude, double longitude, String title) { | ||
+ | PointAnnotationOptions pointAnnotationOptions = new PointAnnotationOptions() | ||
+ | | ||
+ | | ||
+ | | ||
+ | pointAnnotationManager.create(pointAnnotationOptions); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public boolean onMapClick(@NonNull Point point) { | ||
+ | pointAnnotationManager.deleteAll(); | ||
+ | currentPoint = point; | ||
+ | addMarker(point.latitude(), | ||
+ | return false; | ||
+ | } | ||
+ | } | ||
</ | </ | ||
- | {{ youtube> | + | < |
+ | {{ map-view-marker.png | ||
+ | < | ||
- | ==== Marcar | + | ===== Visualizar |
- | Antes de marcar las ubicaciones | + | Para visualizar una ubicación |
+ | * latitud y longitud del punto que queremos visualizar | ||
+ | * Un texto o title que queramos asignar (opcional) | ||
+ | * La imagen que queremos colocar como " | ||
- | Así, suponiendo | + | En Mapbox lo que se solía conocer como '' |
+ | |||
+ | Hay que tener en cuenta que, antes de poder añadir markers, tendremos que inicializar el '' | ||
<code java> | <code java> | ||
. . . | . . . | ||
- | mapaView.getMapAsync(new OnMapReadyCallback() { | + | public class MapsActivity extends AppCompatActivity implements Style.OnStyleLoaded { |
+ | . . . | ||
+ | private PointAnnotationManager pointAnnotationManager; | ||
+ | . . . | ||
+ | protected void onCreate(Bundle savedInstanceState) { | ||
+ | . . . | ||
+ | mapView = findViewById(R.id.mapView); | ||
+ | mapView.getMapboxMap().loadStyleUri(Style.MAPBOX_STREETS, | ||
+ | initializePointAnnotationManager(); | ||
+ | } | ||
+ | |||
+ | private void initializePointAnnotationManager() { | ||
+ | AnnotationPlugin annotationPlugin = AnnotationPluginImplKt.getAnnotations(mapView); | ||
+ | AnnotationConfig annotationConfig = new AnnotationConfig(); | ||
+ | pointAnnotationManager = PointAnnotationManagerKt.createPointAnnotationManager(annotationPlugin, | ||
+ | } | ||
+ | | ||
@Override | @Override | ||
- | public void onMapReady(MapboxMap mapboxMap) { | + | public void onStyleLoaded(@NonNull Style style) { |
- | | + | addMarker(40.1, -0.8, " |
- | .position(new LatLng(latitud, longitud)) | + | |
- | .title(nombre) | + | |
- | | + | |
} | } | ||
+ | | ||
+ | private void addMarker(double latitude, double longitude, String title) { | ||
+ | PointAnnotationOptions pointAnnotationOptions = new PointAnnotationOptions() | ||
+ | .withPoint(Point.fromLngLat(longitude, | ||
+ | .withIconImage(BitmapFactory.decodeResource(getResources(), | ||
+ | .withTextField(title); | ||
+ | pointAnnotationManager.create(pointAnnotationOptions); | ||
+ | } | ||
+ | |||
+ | . . . | ||
}); | }); | ||
- | . . . | ||
</ | </ | ||
Line 1836: | Line 2876: | ||
< | < | ||
- | Y si además queremos | + | Y si además queremos |
+ | |||
+ | En este caso metemos el código en un método para que podamos usarlo siempre que queramos posicionar sobre un punto determinado con una latitud y longitud dadas. También se podrían parametrizar otros valores como el nivel de zoom para poder personalizarlo en cada llamada. | ||
<code java> | <code java> | ||
. . . | . . . | ||
- | CameraPosition position | + | private void setCameraPosition(double latitude, double longitude) { |
- | .target(new LatLng(latitud, longitud)) // Fija la posición | + | CameraOptions cameraPosition |
- | .zoom(17) // Fija el nivel de zoom | + | .center(Point.fromLngLat(longitude, latitude)) |
- | .tilt(30) // Fija la inclinación de la cámara | + | .pitch(45.0) |
- | .build(); | + | .zoom(15.5) |
- | + | .bearing(-17.6) | |
- | mapboxMap.animateCamera(CameraUpdateFactory | + | .build(); |
- | | + | |
+ | } | ||
. . . | . . . | ||
</ | </ | ||
- | ==== Ubicaciones y GPS con Mapbox ==== | + | |
+ | ===== Ubicaciones y GPS con Mapbox | ||
En el siguiente ejemplo se muestra como, utilizando el GPS, podemos acceder a la ubicación actual del usuario. En este caso se calcula su ubicación y se moviliza la cámara del mapa para posicionarnos en ella. Hay que tener en cuenta que puesto en los ejemplos anteriores ya hemos añadido los permisos necesarios para trabajar con el GPS no lo tendremos que hacer ahora. En caso de que no se haya hecho habrá que añadir los permisos en el '' | En el siguiente ejemplo se muestra como, utilizando el GPS, podemos acceder a la ubicación actual del usuario. En este caso se calcula su ubicación y se moviliza la cámara del mapa para posicionarnos en ella. Hay que tener en cuenta que puesto en los ejemplos anteriores ya hemos añadido los permisos necesarios para trabajar con el GPS no lo tendremos que hacer ahora. En caso de que no se haya hecho habrá que añadir los permisos en el '' | ||
Line 1880: | Line 2924: | ||
<code java> | <code java> | ||
- | public class MapActivity extends AppCompatActivity { | + | public class MapActivity extends AppCompatActivity |
private MapView mapaView; | private MapView mapaView; | ||
- | | + | |
- | private FloatingActionButton btUbicacion; | + | |
- | private LocationServices servicioUbicacion; | + | |
@Override | @Override | ||
protected void onCreate(Bundle savedInstanceState) { | protected void onCreate(Bundle savedInstanceState) { | ||
- | . . . | + | . . . |
- | | + | |
- | @Override | + | |
- | | + | |
- | | + | |
- | | + | |
- | } | + | |
- | | + | |
- | + | ||
- | . . . | + | |
- | ubicarUsuario(); | + | |
} | } | ||
- | | + | |
- | | + | |
+ | Location currentLocation = result.getLastLocation(); | ||
+ | Point point = Point.fromLngLat(currentLocation.getLongitude(), | ||
+ | setCameraPosition(point); | ||
+ | addMarker(point, | ||
+ | } | ||
- | servicioUbicacion = LocationServices.getLocationServices(this); | + | @Override |
- | + | public void onFailure(@NonNull Exception exception) { | |
- | | + | } |
- | | + | |
- | | + | private setCameraPosition(Point point) { |
- | public void onClick(View view) { | + | |
- | | + | |
- | | + | |
- | | + | .zoom(13.5) |
- | | + | |
- | + | .build(); | |
- | // Resalta la posición del usuario en el mapa | + | |
- | mapa.setMyLocationEnabled(true); | + | |
- | } | + | |
- | } | + | |
- | }); | + | |
} | } | ||
} | } | ||
Line 1927: | Line 2965: | ||
{{ gps.png?400 }} | {{ gps.png?400 }} | ||
< | < | ||
- | + | ===== Cálculo de rutas con Mapbox | |
- | También existe la posibilidad de implementar un '' | + | |
- | + | ||
- | <code java> | + | |
- | . . . | + | |
- | servicioUbicacion.addLocationListener(new LocationListener() { | + | |
- | @Override | + | |
- | public void onLocationChanged(Location location) { | + | |
- | // Qué hacer cuando la ubicación del usuario cambie | + | |
- | // El parámetro location siempre contiene la nueva ubicación del usuario | + | |
- | } | + | |
- | }); | + | |
- | . . . | + | |
- | </ | + | |
- | + | ||
- | ==== Cálculo de rutas con Mapbox ==== | + | |
< | < | ||
Line 1950: | Line 2973: | ||
Utilizando los servicios de Android de //Mapbox// se pueden calcular las rutas y distancias entre dos puntos dados sobre el mapa, para posteriormente pintarla. | Utilizando los servicios de Android de //Mapbox// se pueden calcular las rutas y distancias entre dos puntos dados sobre el mapa, para posteriormente pintarla. | ||
- | Para ello, lo primero que tenemos que hacer es añadir la correspondiente librería en el '' | + | Para ello, lo primero que tenemos que hacer es añadir la correspondiente librería en el '' |
<code java> | <code java> | ||
Line 1956: | Line 2979: | ||
dependencies { | dependencies { | ||
. . . | . . . | ||
- | compile (' | + | implementation |
- | | + | |
+ | implementation ' | ||
} | } | ||
} | } | ||
Line 1963: | Line 2987: | ||
</ | </ | ||
- | Y a continuación, | + | Y a continuación, |
+ | |||
+ | Hemos implementado la interface | ||
<code java> | <code java> | ||
+ | public class MapsActivity extends AppCompatActivity implements Callback< | ||
. . . | . . . | ||
- | // Calcula la ruta entre el marker y la posición del usuario | + | |
- | private void obtenerRuta(Location markerLocation, Location userLocation) throws ServicesException | + | private void calculateRoute(Point origin, Point destination) { |
+ | RouteOptions routeOptions = RouteOptions.builder() | ||
+ | .baseUrl(Constants.BASE_API_URL) | ||
+ | .user(Constants.MAPBOX_USER) | ||
+ | .profile(DirectionsCriteria.PROFILE_WALKING) | ||
+ | .steps(true) | ||
+ | .coordinatesList(List.of(origin, | ||
+ | .build(); | ||
+ | MapboxDirections directions = MapboxDirections.builder() | ||
+ | .routeOptions(routeOptions) | ||
+ | .accessToken(getString(R.string.mapbox_access_token)) | ||
+ | .build(); | ||
+ | directions.enqueueCall(this); | ||
+ | } | ||
- | | + | |
- | | + | |
- | + | public void onResponse(Call< | |
- | // Obtiene la dirección entre los dos puntos | + | |
- | MapboxDirections direccion | + | |
- | | + | |
- | | + | |
- | .setProfile(DirectionsCriteria.PROFILE_CYCLING) | + | |
- | | + | Log.d(" |
- | | + | |
- | + | ||
- | direccion.enqueueCall(new Callback< | + | |
- | @Override | + | |
- | public void onResponse(Call< | + | |
- | + | ||
- | | + | |
- | | + | |
- | + | ||
- | | + | |
} | } | ||
+ | mapView.getMapboxMap().getStyle(style -> { | ||
+ | LineString routeLine = LineString.fromPolyline(mainRoute.geometry(), | ||
- | @Override | + | GeoJsonSource routeSource = new GeoJsonSource.Builder(" |
- | | + | |
- | | + | .build(); |
- | | + | LineLayer routeLayer = new LineLayer(" |
- | | + | .lineWidth(7.f) |
- | } | + | .lineColor(Color.BLUE) |
+ | .lineOpacity(1f); | ||
+ | | ||
+ | LayerUtils.addLayer(style, | ||
+ | }); | ||
+ | } | ||
- | // Pinta la ruta sobre el mapa | + | @Override |
- | private | + | |
- | + | | |
- | // Recoge los puntos de la ruta | + | |
- | LineString lineString = LineString.fromPolyline(ruta.getGeometry(), | + | |
- | List<Position> coordenadas = lineString.getCoordinates(); | + | |
- | LatLng[] puntos = new LatLng[coordenadas.size()]; | + | |
- | for (int i = 0; i < coordenadas.size(); | + | |
- | | + | |
} | } | ||
- | |||
- | // Pinta los puntos en el mapa | ||
- | mapa.addPolyline(new PolylineOptions() | ||
- | .add(puntos) | ||
- | .color(Color.parseColor("# | ||
- | .width(5)); | ||
- | |||
- | // Resalta la posición del usuario si no lo estaba ya | ||
- | if (!mapa.isMyLocationEnabled()) | ||
- | mapa.setMyLocationEnabled(true); | ||
- | } | ||
. . . | . . . | ||
</ | </ | ||
- | ===== Firebase ===== | ||
- | ==== Registro y configuración de la base de datos ==== | + | ====== Firebase ====== |
- | ==== Registrar información ==== | + | * Plataforma de desarrollo en la nube |
+ | * Nosotros nos centraremos en ver a Firebase como una plataforma de almacenamiento en la nube | ||
+ | * Base de Datos en tiempo real | ||
+ | * Autenticación | ||
- | ==== Consultar información ==== | + | === Configuración de Firebase con Android |
- | ==== Modificar | + | * ¿Dónde empezar? -> https:// |
+ | * Acceder a la consola de Firebase y crear una base de datos (Database): https:// | ||
+ | * Añadir la App a Firebase | ||
+ | * Configuración gradle | ||
+ | * Crear una instancia de Firebase y almacenar | ||
- | ==== Eliminar información ==== | + | === Introducción a Firestore |
- | + | * Base de datos NoSQL | |
- | ===== Creación | + | * Los datos se guardan en colecciones (collections) |
+ | * Cada fila/ | ||
+ | * Cada atributo/ | ||
< | < | ||
- | {{ spring-logo.png }} | + | {{ firestore.png }} |
- | < | + | < |
- | [[http:// | + | ===== Registro |
- | Para eso, lo primero que haremos será utilizar el [[https:// | + | <code groovy> |
- | + | ||
- | Una vez tengamos creado el proyecto inicial, podemos empezar a trabajar en él para tener nuestro servidor. En este caso se trata de crear un servidor que tendrá los servicios web necesarios para que los usuarios de una aplicación Android puedan registrar sus opiniones en nuestra Base de Datos. Así, otros usuarios podrán visualizarlas en sus terminales. | + | |
- | + | ||
- | ==== Configuración del servidor ==== | + | |
- | + | ||
- | Lo primero de todo será editar el fichero de configuración del proyecto para personalizarlo a nuestro caso: | + | |
- | + | ||
- | <file java application.properties> | + | |
- | # Configuración para el acceso a la Base de Datos | + | |
- | spring.jpa.hibernate.ddl-auto=none | + | |
- | spring.jpa.properties.hibernate.globally_quoted_identifiers=true | + | |
- | # Puerto donde escucha el servidor una vez se inicie | + | |
- | server.port=${port: | + | |
- | + | ||
- | # Datos de conexion con la base de datos MySQL | + | |
- | spring.datasource.url=jdbc: | + | |
- | spring.datasource.username=root | + | |
- | spring.datasource.password= | + | |
- | spring.datasource.driverClassName=com.mysql.jdbc.Driver | + | |
- | </ | + | |
- | + | ||
- | Sobre el fichero '' | + | |
- | + | ||
- | <file java build.gradle> | + | |
. . . | . . . | ||
- | apply plugin: 'java' | + | implementation ' |
- | apply plugin: | + | implementation |
- | apply plugin: ' | + | . . . |
- | apply plugin: ' | + | </ |
- | jar { | + | ===== Registrar/ |
- | baseName | + | |
- | version | + | |
- | } | + | |
- | + | ||
- | repositories { | + | |
- | mavenCentral() | + | |
- | } | + | |
- | + | ||
- | dependencies { | + | |
- | compile(' | + | |
- | compile(' | + | |
- | compile(' | + | |
- | providedRuntime(" | + | |
- | } | + | |
- | + | ||
- | configurations { | + | |
- | providedRuntime | + | |
- | } | + | |
- | </ | + | |
- | + | ||
- | Ahora, modificaremos la clase principal '' | + | |
- | + | ||
- | Conviene prestar atención a los comentarios que he dejado en esta clase, donde se explica cómo lanzar la aplicación servidor una vez que este lista. | + | |
- | + | ||
- | <file java Application.java> | + | |
- | /** | + | |
- | * Clase que lanza la aplicación | + | |
- | * | + | |
- | * Cómo compilar/ | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | | + | |
- | * | + | |
- | | + | |
- | | + | |
- | | + | |
- | * | + | |
- | * @author Santiago Faci | + | |
- | * @version curso 2015-2016 | + | |
- | */ | + | |
- | @SpringBootApplication | + | |
- | public class Application extends SpringBootServletInitializer { | + | |
- | + | ||
- | public static void main(String[] args) { | + | |
- | SpringApplication.run(Application.class, | + | |
- | } | + | |
- | + | ||
- | @Override | + | |
- | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { | + | |
- | return application.sources(applicationClass); | + | |
- | } | + | |
- | + | ||
- | private static Class< | + | |
- | } | + | |
- | </ | + | |
- | + | ||
- | ==== Definir la Base de Datos ==== | + | |
- | + | ||
- | Hay que tener en cuenta que //Spring// utiliza por debajo el frame de // | + | |
- | + | ||
- | A continuación se muestra el script '' | + | |
- | + | ||
- | <file sql opiniones.sql> | + | |
- | CREATE DATABASE IF NOT EXISTS opiniones; | + | |
- | USE opiniones; | + | |
- | + | ||
- | CREATE TABLE IF NOT EXISTS opiniones ( | + | |
- | id INT UNSIGNED PRIMARY KEY, | + | |
- | titulo VARCHAR(50) NOT NULL, | + | |
- | texto VARCHAR(50), | + | |
- | fecha DATETIME, | + | |
- | puntuacion INT UNSIGNED | + | |
- | ); | + | |
- | </ | + | |
- | + | ||
- | Así, simplemente tenemos que crear la clase con los atributos y métodos que queramos y añadir las anotaciones que orientarán a // | + | |
<code java> | <code java> | ||
- | /** | + | Book book = new Book(); |
- | * Opinion que los usuarios tienen sobre un monumento | + | book.setId(UUID.randomUUID().toString()); |
- | * Se deben definir las anotaciones que indican la tabla y columnas a las que | + | book.setTitle("El Quijote"); |
- | * representa esta clase y sus atributos | + | book.setPageCount(400); |
- | * | + | |
- | * @author Santiago Faci | + | |
- | * @version curso 2015-2016 | + | |
- | */ | + | |
- | @Entity | + | |
- | @Table(name = "opiniones") | + | |
- | public class Opinion { | + | |
- | @Id | + | FirebaseFirestore db = FirebaseFirestore.getInstance(); db.collection(" |
- | @GeneratedValue | + | |
- | private int id; | + | |
- | @Column | + | |
- | private String titulo; | + | |
- | @Column | + | |
- | private String texto; | + | |
- | @Column | + | |
- | private Date fecha; | + | |
- | @Column | + | |
- | private int puntuacion; | + | |
- | + | ||
- | // Constructor | + | |
- | // Getters y Setters | + | |
- | | + | |
- | } | + | |
</ | </ | ||
- | ==== El Acceso a la Base de Datos ==== | + | < |
+ | {{ books.png }} | ||
+ | < | ||
- | Ahora creamos la '' | + | ===== Consultar |
<code java> | <code java> | ||
- | /** | + | FirebaseFirestore db = FirebaseFirestore.getInstance(); |
- | * Clase que hace de interfaz con la Base de Datos | + | db.collection(" |
- | * Al heredar de CrudRepository se asumen una serie de operaciones | + | |
- | * para registrar o eliminar contenido | + | @Override |
- | * Se pueden añadir operaciones ya preparadas como las que hay de ejemplo ya hechas | + | public |
- | * | + | if (task.isSuccessful()) { |
- | | + | for (QueryDocumentSnapshot document : task.getResult()) { |
- | * @version curso 2015-2016 | + | Book book = document.toObject(Book.class); |
- | */ | + | // TODO Añadir el objeto a una lista, por ejemplo |
- | public | + | |
- | + | } | |
- | | + | }); |
- | | + | |
- | } | + | |
</ | </ | ||
- | ==== Implementación del Controller ==== | + | Se pueden añadir condiciones, antes de invocar al método get(). Por ejemplo: |
- | + | ||
- | Por último, crearemos la clase que hará de '' | + | |
- | + | ||
- | En este caso hemos creado tres operaciones: | + | |
- | + | ||
- | * getOpiniones(): Devuelve todas las opiniones de la base de datos | + | |
- | * getOpiniones(int puntuacion): | + | |
- | * addOpinion(String titulo, String texto, int puntuacion): | + | |
- | + | ||
- | Cada una de las operaciones tienen una URL de mapeo que nos permite acceder a las mismas desde cualquier cliente (navegador, aplicación Java, aplicación Android). Por ejemplo, si quisieramos obtener todas las opiniones que tienen una determinada puntuación utilizaríamos la siguiente URL: http:// | + | |
- | + | ||
- | * http:// | + | |
- | * http:// | + | |
- | * http:// | + | |
<code java> | <code java> | ||
- | /** | + | db.collection("books") |
- | * Controlador para las opiniones | + | .whereGreaterThan("pageCount", 100) |
- | * Contendrá todos los métodos que realicen operaciones sobre opiniones de los usuarios | + | .get() |
- | * | + | |
- | * @author Santiago Faci | + | |
- | * @version curso 2015-2016 | + | |
- | */ | + | |
- | @RestController | + | |
- | public class OpinionController { | + | |
- | + | ||
- | @Autowired | + | |
- | private OpinionRepository repository; | + | |
- | + | ||
- | /** | + | |
- | * Obtiene todas las opiniones de los usuarios | + | |
- | * @return | + | |
- | */ | + | |
- | @RequestMapping("/ | + | |
- | public List< | + | |
- | + | ||
- | List< | + | |
- | return listaOpiniones; | + | |
- | } | + | |
- | + | ||
- | /** | + | |
- | * Obtiene todas las opiniones con una puntuacion determinada | + | |
- | * @param puntuacion | + | |
- | * @return | + | |
- | */ | + | |
- | @RequestMapping("/ | + | |
- | | + | |
- | + | ||
- | List< | + | |
- | return listaOpiniones; | + | |
- | } | + | |
- | + | ||
- | /** | + | |
- | * Registra una nueva opinión en la Base de Datos | + | |
- | * @param titulo | + | |
- | * @param texto | + | |
- | * @param puntuacion | + | |
- | */ | + | |
- | @RequestMapping("/ | + | |
- | | + | |
- | | + | |
- | | + | |
- | + | ||
- | Opinion opinion = new Opinion(); | + | |
- | opinion.setTitulo(titulo); | + | |
- | opinion.setTexto(texto); | + | |
- | opinion.setFecha(new Date(System.currentTimeMillis())); | + | |
- | opinion.setPuntuacion(puntuacion); | + | |
- | + | ||
- | repository.save(opinion); | + | |
- | } | + | |
- | } | + | |
</ | </ | ||
- | ==== Ejecución del servidor ==== | + | Y ordenar o limitar el número de documentos con '' |
- | Una vez terminado todo, para lanzar el servidor tenemos dos opciones: | + | ===== Eliminar información ===== |
- | * Desde el propio IDE, ejecutando '' | + | |
- | * Utilizando el jar que podemos generar con el comando '' | + | |
- | + | ||
- | ==== Lado cliente (Lado Android) | + | |
- | + | ||
- | Desde el lado cliente (aplicación Android en nuestro caso), podremos acceder a los servicios web mediante la URL que hayamos definido de la siguiente forma, y siempre dentro de una '' | + | |
- | + | ||
- | * Para obtener todas las opiniones de la Base de Datos en el servidor: | + | |
<code java> | <code java> | ||
- | . . . | + | FirebaseFirestore db = FirebaseFirestore.getInstance(); |
- | private List< | + | db.collection("books" |
- | private final String URL_SERVIDOR = " | + | .delete() |
- | . . . | + | |
- | RestTemplate restTemplate = new RestTemplate(); | + | @Override |
- | restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter()); | + | public void onSuccess(Void aVoid) { |
- | Opinion[] opinionesArray = restTemplate.getForObject(URL_SERVIDOR + "/opiniones", | + | // TODO Documento eliminado correctamente |
- | listaOpiniones.addAll(Arrays.asList(opinionesArray)); | + | } |
- | . . . | + | }) |
+ | .addOnFailureListener(new OnFailureListener() { | ||
+ | @Override | ||
+ | public void onFailure(@NonNull Exception e) { | ||
+ | // TODO Error eliminando documento | ||
+ | } | ||
+ | }); | ||
</ | </ | ||
- | * O bien para registrar una nueva opinión en el servidor desde nuestro dispositivo móvil: | + | ======= Arquitectura MVP ======= |
- | <code java> | + | <figure> |
- | . . . | + | {{ mvp.png? |
- | RestTemplate restTemplate = new RestTemplate(); | + | < |
- | restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter()); | + | |
- | restTemplate.getForObject(URL_SERVIDOR + "/ | + | |
- | "& | + | |
- | . . . | + | |
- | </ | + | |
- | + | ||
- | Aunque antes de poder utilizar las llamadas a los métodos que permiten invocar los servicios web de nuestro servidor, tendremos que añadir un par de dependencias a nuestro '' | + | |
- | + | ||
- | <code java> | + | |
- | . . . | + | |
- | android | + | |
- | . . . | + | |
- | packagingOptions | + | |
- | exclude " | + | |
- | exclude " | + | |
- | exclude " | + | |
- | exclude " | + | |
- | | + | |
- | . . . | + | |
- | } | + | |
- | . . . | + | |
- | dependencies { | + | |
- | . . . | + | |
- | compile ' | + | |
- | compile ' | + | |
- | . . . | + | |
- | } | + | |
- | . . . | + | |
- | </code> | + | |
- | + | ||
- | {{ youtube>TBIzVT5dHC4 }} | + | |
- | \\ | + | |
---- | ---- | ||
- | ===== Ejercicios ===== | + | ====== Ejercicios |
{{ ejercicio.png? | {{ ejercicio.png? | ||
Line 2359: | Line 3161: | ||
---- | ---- | ||
- | ===== Proyectos de ejemplo ===== | + | ====== Proyectos de ejemplo ====== |
- | + | ||
- | Todos los proyectos de ejemplo se pueden encontrar en el [[https:// | + | |
- | + | ||
- | Todos los ejercicios que vayamos haciendo en clase se pueden encontrar en el [[https:// | + | |
- | + | ||
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | * [[https:// | + | |
- | + | ||
- | ===== Práctica 1.1 ===== | + | |
- | + | ||
- | === Objetivos === | + | |
- | + | ||
- | Desarrollar una aplicación para un dispositivo móvil Android. | + | |
- | + | ||
- | === Herramientas Necesarias === | + | |
- | + | ||
- | * Android Studio | + | |
- | * IntelliJ IDEA (para la implementación del servidor) | + | |
- | * Pencil (para el prototipado de las pantallas) | + | |
- | + | ||
- | === Enunciado === | + | |
- | + | ||
- | Debes diseñar e implementar una aplicación para dispositivo móvil Android que cumpla, al menos, con los requisitos que se indican como mínimos. Hay que tener en cuenta que la aplicación se deberá poder lanzar sobre móviles cuyo SDK sea cualquier de la rama 5.X.X (o en adelante) de Android. | + | |
- | + | ||
- | El tema de la aplicación debe ser escogido por el alumno, que lo tendrá que notificar al menos con dos semanas de antelación a la entrega de la práctica. En ese momento se debe presentar un documento donde se describa la aplicación a desarrollar, | + | |
- | + | ||
- | === Requisitos Mínimos (1 pto cada uno) === | + | |
- | + | ||
- | * La aplicación contará con, al menos, 4 Activities o Fragments, utilizando controles '' | + | |
- | * Se deberán usar Bases de Datos para almacenar información. El usuario deberá ser capaz de registrar, modificar y visualizar en un '' | + | |
- | * La aplicación contará con un menú de opciones o '' | + | |
- | * Añadir alguna función que interactúe con otras aplicaciones del dispositivo | + | |
- | * Diseñar para alguna '' | + | |
- | + | ||
- | === Otras funcionalidades (1 pto cada una) === | + | |
- | + | ||
- | * Utilizar en diferentes '' | + | |
- | * Gestionar el proyecto utilizando las herramientas proporcionadas por //GitHub// o // | + | |
- | * Usar imágenes como atributos de algún objeto | + | |
- | * Añadir la opción de eliminar objetos de la lista | + | |
- | * Utilizar diálogos siempre que sea necesario | + | |
- | + | ||
- | === Observaciones === | + | |
- | + | ||
- | Para la entrega se creará un repositorio con el código del proyecto y una descarga que permita acceder directamente al APK de una versión instalable de la aplicación. | + | |
- | + | ||
- | ===== Práctica 1.2 ===== | + | |
- | + | ||
- | === Objetivos === | + | |
- | + | ||
- | Desarrollar una aplicación para un dispositivo móvil Android. | + | |
- | + | ||
- | === Herramientas Necesarias === | + | |
- | + | ||
- | * Android Studio | + | |
- | * IntelliJ IDEA (para la implementación del servidor) | + | |
- | * Spring Boot | + | |
- | * Pencil (para el prototipado de las pantallas) | + | |
- | + | ||
- | === Enunciado === | + | |
- | + | ||
- | Sobre la aplicación desarrollada para la práctica 1.1, se deberán añadir nuevas funcionalidades. | + | |
- | + | ||
- | === Requisitos Mínimos (1 pto cada uno) === | + | |
- | + | ||
- | * Añadir otras 4 Activities a la aplicación | + | |
- | * Se mostrará información útil para la aplicación en un mapa (con Google Maps o MapBox), de forma que pueda interactuarse con él para llevar alguna acción de utilidad para la aplicación. | + | |
- | * Utilizar y generar datos en la aplicación disponibles a través de un servidor (implementado con '' | + | |
- | * Añadir un menú de preferencias con al menos 3 opciones que modifiquen el comportamiento de la aplicación. Este menú de preferencias estará accesible en todo momento desde el '' | + | |
- | * Utilizar el GPS del dispositivo para realizar alguna función sobre el mapa de la aplicación. | + | |
- | + | ||
- | === Otras funcionalidades (1 pto cada una) === | + | |
- | + | ||
- | * Diseñar algún servicio que interactúe con el usuario mediante notificaciones del sistema. | + | |
- | * Añadir alguna función que interactúe con otras aplicaciones del dispositivo | + | |
- | * Presentar la aplicación utilizando pestañas | + | |
- | * Permitir que en la aplicación puedan interactuar, | + | |
- | * Utilizar en diferentes '' | + | |
- | * Gestionar el proyecto utilizando las herramientas proporcionadas por //GitHub// o // | + | |
- | + | ||
- | === Observaciones | + | |
- | Para la entrega se creará un repositorio | + | [[https:// |
+ | * **BiziStations**: | ||
+ | * **BottomNavigation**: | ||
+ | * **Drawer**: Drawer sample | ||
+ | * **FileSystem**: | ||
+ | * **Fragments**: | ||
+ | * **Fragments2**: | ||
+ | * **GPS**: GPS sample project | ||
+ | * **Mapbox**: Mapbox library sample project (load map, add markers and set camera position) | ||
+ | * **Maps**: Maps sample project: Google Maps, Markers and Directions API | ||
+ | * **MasterDetail** (Fragments): | ||
+ | * **Notifications**: | ||
+ | * **Permissions**: | ||
+ | * **Preferences**: | ||
+ | * **RecyclerView**: | ||
+ | * **Room database**: CRUD sample using Room library | ||
+ | * **themealdb**: | ||
---- | ---- | ||
- | (c) 2016-2020 Santiago Faci | + | (c) 2016-2023 Santiago Faci |
apuntes/android.1595922833.txt.gz · Last modified: 28/07/2020 07:53 by Santiago Faci