Mobil Programlama

Android

Guidex Uygulaması

Lisans: Creative Commons 26.11.2020 tarihinde güncellendi
Bakabileceğiniz Etiketler: Eğitmen: Geleceği Yazanlar Ekibi

Bu uygulamada sizlere uzaktaki bir JSON kaynaktan çekilen POI (Point of Interest - ilgi çekici nokta) listesini öncelikle bir tabloda daha sonra da bir harita üzerinde nasıl göstereceğinizi anlatacağız. Yazıyı okumaya başlamadan önce aşağıdaki konuları okumanızı tavsiye ederiz;

Uygulamamız uzaktaki bir JSON servisinde yer alan veriyi kullanarak kullanıcıya ilginç mekânları gösterecektir. Mekânları görmek için kullanıcı ister liste görünümünden isterse de harita görünümünden faydalanabilir. Harita görünümü seçildiğinde mekânlar koordinatlarına göre harita üzerinde pin olarak görüntülenecektir. Uygulamada konu anlatımlarından farklı olarak profesyonel bir tasarımcıdan gelen tasarımları kullanacağız ve bunları da giydirmeyi göreceğiz.

Öncelikle tasarımcımızın bize gönderdiği kesilmiş PNG dosyalarını ve tasarımı giydirirken bize kılavuzluk edecek genel görünüm dosyasını gözden geçirelim.

Gördüğünüz gibi tasarımcımız genel görünümle beraber görsel öğelerin yüksekliklerini, ebatlarını ve bunlarla beraber yazıların fontlarını ve büyüklüklerini detaylı bir şekilde anlatmış. Siz de projenizde profesyonel bir tasarımcıyla çalışıyorsanız bu şekilde direktifler beklemeye hazır olun (göndermezse de göndermesini talep edin).

Bazı tasarımcılar PSD dosyalarının genel görünümünü PNG olarak kayıt edip özellikleri üzerine yazmayı tercih ederken; bazı tasarımcılar web üzerinde yardımcı sitelere genel tasarımı koyarak uygulama detaylarını anlatırlar. Metodlar farklı gösterse de geliştirici için önemli olan kodlarken uyması gereken kurallardır ve ebat, renk RGB değeri, kullanılan yazıtipi (font) gibi bilgileri tüm detaylarıyla bilmesi gerekir.

Şimdi uygulamaya başlamak için Android Studio içerisinden yeni bir proje oluşturalım ve bir adet Activity dosyası (MainActivity) ekleyelim. Proje sihirbazında yeni Activity oluşturma ekranında kolaylık olması açısından Navigation Type değeri için Scrollable Tabs seçeneğini seçmemiz, Android'in otomatik olarak uygulamayı iki ekrana bölmesini sağlayacaktır.

Bu şekilde tasarımcının istediği görüntüye büyük ölçüde yaklaşmış oluruz. MainActivity ekranına ait XML dosyasında ise üst barla ilgili renk değişiklikleri aşağıdaki gibi gerçekleşir;

 



<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <android.support.v4.view.PagerTabStrip
        android:id="@+id/pager_title_strip"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="top"
        android:background="#fff"
        android:padding="4dp"
        android:textColor="#31B881" />

</android.support.v4.view.ViewPager>

 

MainActivity ekranı bu noktada diğer iki küçük ekranı kapsayan bir ana ekran vazifesi üstlenecektir.  MainActivity’e ait kod aşağıdaki gibidir;

 




package com.turkcell.guidex;

import java.util.Locale;

import android.app.ActionBar;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;

import com.turkcell.guidex.asycntask.DownloadTask;
import com.turkcell.guidex.fragment.ListFragment;
import com.turkcell.guidex.fragment.MapListFragment;

public class MainActivity extends FragmentActivity implements ActionBar.TabListener {

   SectionsPagerAdapter    mSectionsPagerAdapter;

   ViewPager               mViewPager;

   private ListFragment    listFragment;
   private MapListFragment mapFragment;

   private DownloadTask    downloadTask;

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

      listFragment = new ListFragment();
      mapFragment = new MapListFragment();

      downloadTask = new DownloadTask();
      downloadTask.addListener(listFragment);
      downloadTask.addListener(mapFragment);

      final ActionBar actionBar = getActionBar();
      actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

      mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

      mViewPager = (ViewPager) findViewById(R.id.pager);
      mViewPager.setAdapter(mSectionsPagerAdapter);

      mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
         @Override
         public void onPageSelected(int position) {
            actionBar.setSelectedNavigationItem(position);
         }
      });

      for (int i = 0; i < mSectionsPagerAdapter.getCount(); i++) {
         actionBar.addTab(actionBar.newTab().setText(mSectionsPagerAdapter.getPageTitle(i)).setTabListener(this));
      }

      downloadTask.execute((Void) null);
   }

   @Override
   protected void onDestroy() {
      if (downloadTask != null) {
         downloadTask.cancel(true);
         downloadTask.removeListener(listFragment);
         downloadTask.removeListener(mapFragment);
      }

      super.onDestroy();
   }

   @Override
   public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
      mViewPager.setCurrentItem(tab.getPosition());
   }

   @Override
   public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
   }

   @Override
   public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
   }

   public class SectionsPagerAdapter extends FragmentPagerAdapter {

      public SectionsPagerAdapter(FragmentManager fm) {
         super(fm);
      }

      @Override
      public Fragment getItem(int position) {
         switch (position) {
            case 0 :
               return listFragment;
            case 1 :
               return mapFragment;
            default :
               return new ListFragment();
         }
      }

      @Override
      public int getCount() {
         return 2;
      }

      @Override
      public CharSequence getPageTitle(int position) {
         Locale l = Locale.getDefault();
         switch (position) {
            case 0 :
               return getString(R.string.title_section1).toUpperCase(l);
            case 1 :
               return getString(R.string.title_section2).toUpperCase(l);
         }
         return null;
      }
   }

}

 

Sınıf içerisindeki değişkenlere göz atarsak;

  • sectionPagerAdapter : Ekranda oluşturulacak alt ekranları kontrol etmeye yarayan ve FragmentPagerAdapter ile oluşturulmuş sınıftır. Burada listeleme ve harita ekranlar nerisinde yatay eksende oluşması sağlanır.
  • mViewPager : Layout dosyası içerisinde tanımlanan alt ekranları taşıyacak sınıftır.
  • listFragment : Bizim oluşturduğumuz ve içerisinde bir adet ListView barındıran bir sınıftır.
  • mapFragment : Bizim oluşturduğumuz ve içerisinde bir adet MapView barındıran bir sınıftır.
  • downloadTask : JSON servisinden verileri asenkron ekilmesi için bizim oluşturduğumuz bir sınıftır.

MainActivity onCreate metodu içerisinde yukarıdaki değişkenlerle ilgili gerekli atamaları yaptıktan sonra mViewPager için SectionPagerAdapter adlı sınıfı tanımlayarak ekranın oluşmasını sağlıyoruz. SectionPagerAdapter sınıfı ise getItem metodu ile hangi sekmeye karşılık hangi ekranın geleceğini belirliyor. Burada switch kontrol mekanizması içerisinde oluşturmak istediğimiz alt ekranları belirliyoruz. İlk sırada listFragment değişkeni ikinci sırada ise mapFragment değişkeni ile ana ekran sekmeleri oluşturuyor.

İlk sekmemiz olan ListFragment sınıfı aşağıdaki gibidir;

 




package com.turkcell.guidex.fragment;

import java.util.List;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;

import com.turkcell.guidex.R;
import com.turkcell.guidex.adapter.PoiListAdapter;
import com.turkcell.guidex.asycntask.DownloadTaskCallback;
import com.turkcell.guidex.model.Poi;

public class ListFragment extends Fragment implements DownloadTaskCallback {
   private ListView poiListView;

   @Override
   public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

      View rootView = inflater.inflate(R.layout.list, container, false);
      poiListView = (ListView) rootView.findViewById(R.id.poiListView);

      return rootView;
   }

   @Override
   public void poisRefreshed(List<Poi> pois) {
      poiListView.setAdapter(new PoiListAdapter(getActivity(), pois));
   }
}

 

ListFragment içerisinde görüntü oluşturulduktan sonra ekrandaki ListView'i poiListView değişkeniyle ilişkilendiriyoruz. Az sonra anlatacağımız DownloadTask sınıfı, mekân bilgilerini çektikten sonra Fragment'lar içerisinde yer alan poisRefreshed metodunu uyarır ve bu sayede poiListView ile tanımladığımız listeyi doldurabiliriz.

NOT: Ekran satırlarını oluşturmak için PoiListAdapter adında özel oluşturulan bir BaseAdapter’dan faydalanılmıştır. PoiListAdapter satırların tasarımını oluşturmak ve değerlerini belirlemekle görevlidir. Hatırlarsanız benzer bir yöntemi Reader uygulamasında  ve ListView özelleştirme konusunda kullanmıştık.

Diğer sekmemiz MapListFragment ise aşağıdaki gibidir;

 




package com.turkcell.guidex.fragment;

import java.util.List;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapsInitializer;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;
import com.turkcell.guidex.R;
import com.turkcell.guidex.asycntask.DownloadTaskCallback;
import com.turkcell.guidex.model.Poi;

public class MapListFragment extends Fragment implements DownloadTaskCallback {

   private GoogleMap map;

   @Override
   public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

      try {
         MapsInitializer.initialize(getActivity());
      }
      catch (GooglePlayServicesNotAvailableException e) {
      }

      View rootView = inflater.inflate(R.layout.map, container, false);
      map = ((com.google.android.gms.maps.SupportMapFragment) getFragmentManager().findFragmentById(R.id.map)).getMap();

      return rootView;
   }

   @Override
   public void poisRefreshed(List<Poi> pois) {
      for (Poi poi : pois) {
         map.addMarker(new MarkerOptions().position(new LatLng(poi.getLatitude(), poi.getLongitude()))
                                          .title(poi.getName())
                                          .icon(BitmapDescriptorFactory.fromResource(R.drawable.pin)));
      }
   }
}

 

Harita konusunda hatırlarsanız GoogleMap değişkeni ekranda görüntülenen haritayı temsil eden sınıfa verilen isimdi. Öncelikle bu değişkeni map.xml layout dosyası içerisinde tanımladığımız harita ile eşleştirme yapıyoruz. Daha sonra DownloadTask mekan listesini yüklemeyi bitirdiğinde poisRefreshed metodu uyarılıyor ve elimize mekan listesi ulaşmış oluyor. Harita içinde yer alan addMarker metodu ile harita üzerine mekanları tek tek yerleştiriyoruz. Tasarımcı mekanlar için özel bir pin tasarımı yaptığından drawable klasörü içerisinde yer alan pin.png dosyasını kullanmak için fromResource metoduna ihtiyaç duyarız. Bu sayede ekranda gösterilen mekânlar klasik pinlerden farklı bir tasarıma sahip olacaktır.

DownloadTask sınıfı ise mekânları web servisten arka planda yüklememizi sağlar. Bu sayede kullanıcı deneyimine zarar vermeden internet bağlantı işlemlerini halletmiş oluruz. DownloadTask sınıfına ait kod aşağıdaki gibidir;

 




package com.turkcell.guidex.asycntask;

import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.os.AsyncTask;

import com.turkcell.guidex.DownloadHelper;
import com.turkcell.guidex.model.Poi;

public class DownloadTask extends AsyncTask<Void, Void, List<Poi>> {

   private static final String        poiUrl = "http://www.bilmiyordum.com/poi.json";

   private List<DownloadTaskCallback> listenerArray;

   public DownloadTask() {
      this.listenerArray = new ArrayList<DownloadTaskCallback>();
   }

   public void addListener(DownloadTaskCallback callback) {
      this.listenerArray.add(callback);
   }

   public void removeListener(DownloadTaskCallback callback) {
      this.listenerArray.remove(callback);
   }

   @Override
   protected List<Poi> doInBackground(Void... params) {
      String jsonString = DownloadHelper.getContents(poiUrl);
      List<Poi> poiList = new ArrayList<Poi>();
      try {
         JSONArray jsonArray = new JSONArray(jsonString);
         for (int i = 0; i < jsonArray.length(); i++) {
            JSONObject jsonObject = jsonArray.getJSONObject(i);
            poiList.add(Poi.createFromJSON(jsonObject));
         }
      }
      catch (JSONException je) {
      }
      return poiList;
   }

   @Override
   protected void onPostExecute(List<Poi> result) {
      for (DownloadTaskCallback callback : listenerArray) {
         callback.poisRefreshed(result);
      }
      super.onPostExecute(result);
   }

}

 

listenerArray DownloadTask işlemini bitirdiğinde uyarılacak diğer sınıfların tutulduğu bir dizidir. İşlemi takip eden sınıfları addListener metodu yardımıyla bu diziye ekleyebiliriz. onPostExecute metodunda bu dizi içerisinde yer alan her sınıfa elde ettiğimiz poi listesini yollamak için poisRefreshed metodunu çağıracağız.

removeListener metodu bütün işlemler bittikten sonra MainActivity içerisinde çağırılır ve DownloadTask yok edildikten sonra diğer sınıfların dinlemeye devam etmesinin önüne geçilir.

İnternet bağlantı işlemini ise ana akışı engellememek için doInBackground metodu içerisinde gerçekleştireceğiz. Burada yapılan işlem poiUrl ile belirtilen adresten JSON metni çekildikten sonra JSONArray sınıfı yardımıyla bu değerlerin birer objeye dönüştürülmesidir. Daha sonra bu JSON objeleri bizim tanımladığımız Poi sınıfına çevirilir ve listeye atılır. Poi sınıfı aşağıda verilmiştir;

 



package com.turkcell.guidex.model;

import org.json.JSONException;
import org.json.JSONObject;

/**
 * @author ouysal
 * 
 */
public class Poi
{
   private String name;
   private double latitude;
   private double longitude;

   @Override
   public String toString() {
      return this.name;
   }

   /**
    * @param jsonObject
    *            a poi from a JSON Object
    * @return a new poi
    */
   public static Poi createFromJSON(JSONObject jsonObject) {
      Poi poi = new Poi();
      try {
         poi.setName(jsonObject.getString("name"));
         poi.setLatitude(jsonObject.getDouble("lat"));
         poi.setLongitude(jsonObject.getDouble("lon"));
      } catch (JSONException je) {
      }
      return poi;
   }

   /**
    * @return the name
    */
   public String getName() {
      return name;
   }

   /**
    * @param name
    *            the name to set
    */
   public void setName(String name) {
      this.name = name;
   }

   /**
    * @return the latitude
    */
   public double getLatitude() {
      return latitude;
   }

   /**
    * @param latitude
    *            the latitude to set
    */
   public void setLatitude(double latitude) {
      this.latitude = latitude;
   }

   /**
    * @return the longitude
    */
   public double getLongitude() {
      return longitude;
   }

   /**
    * @param longitude
    *            the longitude to set
    */
   public void setLongitude(double longitude) {
      this.longitude = longitude;
   }

}

 

Mekâna ait isim, enlem ve boylam bilgilerini içeren Poi sınıfı, createFromJSON adlı statik metod yardımıyla verilen bir JSON objesinden Poi objesi üretir. Bu sayede biz de DownloadTask içerisinde bulunan JSON objelerini kolaylıkla Poi objelerine çevirebiliriz.

AndroidManifest dosyamız ise aşağıdaki gibidir. Burada haritalar konusundan hatırlayacağımız tanımlamaları yapmayı unutmayalım.

 



<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.turkcell.guidex"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="15"
        android:targetSdkVersion="18" />

    <permission
        android:name="com.turkcell.guidex.permission.MAPS_RECEIVE"
        android:protectionLevel="signature" />

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="com.turkcell.guidex.permission.MAPS_RECEIVE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />

    <uses-feature
        android:name="android.hardware.location"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.location.network"
        android:required="false" />
    <uses-feature android:name="android.hardware.location.gps" />
    <uses-feature
        android:name="android.hardware.wifi"
        android:required="false" />
    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/pin"
        android:label="@string/app_name"
         >
        <activity
            android:name="com.turkcell.guidex.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="AIzaSyDv36aLpz1KOSL2_kOyt9WZMRlNI9AmytY" />
        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
    </application>

</manifest>