블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 받은 트랙백

글 보관함

calendar

1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        

Udacity 강좌 - Lesson 2 실습 05

2016.08.29 03:00 | Posted by 솔웅


안드로이드에서 메뉴는 3가지 종류가 있음
- Options menu and app bar
- Context menu and contextual action mode
- Popup menu


https://developer.android.com/guide/topics/ui/menus.html


예제

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
   
<item android:id="@+id/new_game"
         
android:icon="@drawable/ic_new_game"
         
android:title="@string/new_game"
         
android:showAsAction="ifRoom"/>
   
<item android:id="@+id/help"
         
android:icon="@drawable/ic_help"
         
android:title="@string/help" />
</menu>

<item> 에는 몇가지 attribute 들이 있다.

android:id
A resource ID that's unique to the item, which allows the application to recognize the item when the user selects it.
android:icon
A reference to a drawable to use as the item's icon.
android:title
A reference to a string to use as the item's title.
android:showAsAction
Specifies when and how this item should appear as an action item in the app bar.

Sunshine source code 의 메뉴 xml을 보면 아래와 같이 되어 있다.


여기서 보면 title attribute 가 있는데 여기에 Settings 라고 돼 있음


* Refresh Button


forecastfragment.xml 이라는 새로운 메뉴 레이아웃을 만든다. 여기에는 아이디가 action_refresh 라는 아이템을 만든다. Label 은 Refresh 라고 한다.<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/action_refresh"
        android:title="@string/action_refresh"
        app:showAsAction="never" />
</menu>


Strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!--
         Used in Action Bar, and in AndroidManifest to tell the device the name of this app.
         It's to keep this short, so your launcher icon isn't displayed with "The greatest Wea"
         or something similar.
    -->
    <string name="app_name">Sunshine</string>

    <!--
         By convention, "action" denotes that this String will be used as the label for an Action,
         typically from the action bar.  The ActionBar is limited real estate, so shorter is better.
    -->
    <string name="action_settings">Settings</string>

    <!-- Menu label to fetch updated weather info from the server -->
    <string name="action_refresh" translatable="false">Refresh</string>
    <string name="hello_world">Hello world!</string>

</resources>


* forecastFragment에 있는 옵션 메뉴 inflate 하기
* onOptionsItemSelected() 셋업하기
* Fragment에 있는 setHasOptionsMenu(true) 메소드를 호출 함으로서 메뉴 옵션이 있다는 것을 알린다.



위 소스 코드는 2과를 모두 완료 했을 때의 코드임.

onCreate() 에서 setHasOptionsMenu(true) 를 함으로서 옵션 메뉴가 있다는 것을 생성 단계에 알리고 onCreateOptionsMenu() 에서 forecastfragment xml 파일에 있는 메뉴를 inflate 합니다.

그 다음에 해당 메뉴가 선택됐을 때 무엇을 할 지는 onOptionsItemSelected() 에서 구현 합니다.


여기서 보면 해당 메뉴 (Refresh - 아이디는 action_refresh) 가 선택 되면 FetchWeatherTask() 클래스를 호출하고 이 때 파라미터로 55347 이라는 우편번호를 전달해 줍니다.


그럼 사용자가 이 메뉴를 선택하면 호출된 클래스에 구현된 내용들이 실행될 겁니다.


전 글에서 이 FetchWeatherTask 클래스는 구현해 놨었죠?


여기서 한가지 추가해야 할 것은 날씨 싸이트와 소통을 하려면 안드로이드에서는 퍼미션을 세팅해 줘야 합니다.


이 퍼미션은 Manifest xml파일에서 users-permission을 세팅해야 합니다.



여기서는 android.permission.INTERNET 을 추가해야 합니다.


위에보시면 해당 라인이 추가 돼 있는 것을 볼 수 있습니다.



이제 Refresh 버튼을 누르면 FetchWeatherTask 를 실행 할 텐데요.

여기에 우편번호를 받아서 날씨 싸이트에 전달하고 이것을 JSON 형식으로 받은 후 파싱해서 화면에 뿌려줄 수 있도록 만듭니다.



   public class FetchWeatherTask extends AsyncTask<String, Void, String[]> {

        private final String LOG_TAG = FetchWeatherTask.class.getSimpleName();

        /* The date/time conversion code is going to be moved outside the asynctask later,
         * so for convenience we're breaking it out into its own method now.
         */
        private String getReadableDateString(long time){
            // Because the API returns a unix timestamp (measured in seconds),
            // it must be converted to milliseconds in order to be converted to valid date.
            SimpleDateFormat shortenedDateFormat = new SimpleDateFormat("EEE MMM dd");
            return shortenedDateFormat.format(time);
        }

        /**
         * Prepare the weather high/lows for presentation.
         */
        private String formatHighLows(double high, double low) {
            // For presentation, assume the user doesn't care about tenths of a degree.
            long roundedHigh = Math.round(high);
            long roundedLow = Math.round(low);

            String highLowStr = roundedHigh + "/" + roundedLow;
            return highLowStr;
        }

        /**
         * Take the String representing the complete forecast in JSON Format and
         * pull out the data we need to construct the Strings needed for the wireframes.
         *
         * Fortunately parsing is easy:  constructor takes the JSON string and converts it
         * into an Object hierarchy for us.
         */
        private String[] getWeatherDataFromJson(String forecastJsonStr, int numDays)
                throws JSONException {

            // These are the names of the JSON objects that need to be extracted.
            final String OWM_LIST = "list";
            final String OWM_WEATHER = "weather";
            final String OWM_TEMPERATURE = "temp";
            final String OWM_MAX = "max";
            final String OWM_MIN = "min";
            final String OWM_DESCRIPTION = "main";

            JSONObject forecastJson = new JSONObject(forecastJsonStr);
            JSONArray weatherArray = forecastJson.getJSONArray(OWM_LIST);

            // OWM returns daily forecasts based upon the local time of the city that is being
            // asked for, which means that we need to know the GMT offset to translate this data
            // properly.

            // Since this data is also sent in-order and the first day is always the
            // current day, we're going to take advantage of that to get a nice
            // normalized UTC date for all of our weather.

            Time dayTime = new Time();
            dayTime.setToNow();

            // we start at the day returned by local time. Otherwise this is a mess.
            int julianStartDay = Time.getJulianDay(System.currentTimeMillis(), dayTime.gmtoff);

            // now we work exclusively in UTC
            dayTime = new Time();

            String[] resultStrs = new String[numDays];
            for(int i = 0; i < weatherArray.length(); i++) {
                // For now, using the format "Day, description, hi/low"
                String day;
                String description;
                String highAndLow;

                // Get the JSON object representing the day
                JSONObject dayForecast = weatherArray.getJSONObject(i);

                // The date/time is returned as a long.  We need to convert that
                // into something human-readable, since most people won't read "1400356800" as
                // "this saturday".
                long dateTime;
                // Cheating to convert this to UTC time, which is what we want anyhow
                dateTime = dayTime.setJulianDay(julianStartDay+i);
                day = getReadableDateString(dateTime);

                // description is in a child array called "weather", which is 1 element long.
                JSONObject weatherObject = dayForecast.getJSONArray(OWM_WEATHER).getJSONObject(0);
                description = weatherObject.getString(OWM_DESCRIPTION);

                // Temperatures are in a child object called "temp".  Try not to name variables
                // "temp" when working with temperature.  It confuses everybody.
                JSONObject temperatureObject = dayForecast.getJSONObject(OWM_TEMPERATURE);
                double high = temperatureObject.getDouble(OWM_MAX);
                double low = temperatureObject.getDouble(OWM_MIN);

                highAndLow = formatHighLows(high, low);
                resultStrs[i] = day + " - " + description + " - " + highAndLow;
            }

            for (String s : resultStrs) {
                Log.v(LOG_TAG, "Forecast entry: " + s);
            }
            return resultStrs;

        }
        @Override
        protected String[] doInBackground(String... params) {

            // If there's no zip code, there's nothing to look up.  Verify size of params.
            if (params.length == 0) {
                return null;
            }

            // These two need to be declared outside the try/catch
            // so that they can be closed in the finally block.
            HttpURLConnection urlConnection = null;
            BufferedReader reader = null;

            // Will contain the raw JSON response as a string.
            String forecastJsonStr = null;

            String format = "json";
            String units = "metric";
            int numDays = 7;

            try {
                // Construct the URL for the OpenWeatherMap query
                // Possible parameters are avaiable at OWM's forecast API page, at
                // http://openweathermap.org/API#forecast
                final String FORECAST_BASE_URL =
                        "http://api.openweathermap.org/data/2.5/forecast/daily?";
                final String QUERY_PARAM = "q";
                final String FORMAT_PARAM = "mode";
                final String UNITS_PARAM = "units";
                final String DAYS_PARAM = "cnt";
                final String APPID_PARAM = "APPID";

                Uri builtUri = Uri.parse(FORECAST_BASE_URL).buildUpon()
                        .appendQueryParameter(QUERY_PARAM, params[0])
                        .appendQueryParameter(FORMAT_PARAM, format)
                        .appendQueryParameter(UNITS_PARAM, units)
                        .appendQueryParameter(DAYS_PARAM, Integer.toString(numDays))
                        //.appendQueryParameter(APPID_PARAM, BuildConfig.OPEN_WEATHER_MAP_API_KEY)
                        .appendQueryParameter(APPID_PARAM, "84ac1b7cfb5b87edaa1fa4626d0fcbc8")
                        .build();

                URL url = new URL(builtUri.toString());

                Log.v(LOG_TAG, "Built URI " + builtUri.toString());

                // Create the request to OpenWeatherMap, and open the connection
                urlConnection = (HttpURLConnection) url.openConnection();
                urlConnection.setRequestMethod("GET");
                urlConnection.connect();

                // Read the input stream into a String
                InputStream inputStream = urlConnection.getInputStream();
                StringBuffer buffer = new StringBuffer();
                if (inputStream == null) {
                    // Nothing to do.
                    return null;
                }
                reader = new BufferedReader(new InputStreamReader(inputStream));

                String line;
                while ((line = reader.readLine()) != null) {
                    // Since it's JSON, adding a newline isn't necessary (it won't affect parsing)
                    // But it does make debugging a *lot* easier if you print out the completed
                    // buffer for debugging.
                    buffer.append(line + "\n");
                }

                if (buffer.length() == 0) {
                    // Stream was empty.  No point in parsing.
                    return null;
                }
                forecastJsonStr = buffer.toString();

                Log.v(LOG_TAG, "Forecast string: " + forecastJsonStr);
            } catch (IOException e) {
                Log.e(LOG_TAG, "Error ", e);
                // If the code didn't successfully get the weather data, there's no point in attemping
                // to parse it.
                return null;
            } finally {
                if (urlConnection != null) {
                    urlConnection.disconnect();
                }
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (final IOException e) {
                        Log.e(LOG_TAG, "Error closing stream", e);
                    }
                }
            }

            try {
                return getWeatherDataFromJson(forecastJsonStr, numDays);
            } catch (JSONException e) {
                Log.e(LOG_TAG, e.getMessage(), e);
                e.printStackTrace();
            }

            // This will only happen if there was an error getting or parsing the forecast.
            return null;
        }

        @Override
        protected void onPostExecute(String[] result) {
            if (result != null) {
                mForecastAdapter.clear();
                for(String dayForecastStr : result) {
                    mForecastAdapter.add(dayForecastStr);
                }
                // New data is back from the server.  Hooray!
            }
        }
    }


코드를 쭉 훑어 보면은요.


우선 LOG_TAG 에 현재 클래스 이름을 담고 getReadableDateString() 메소드를 private 으로 만듭니다.

여기서는 날짜 정보를 받아서 이것을 특정 형식에 넣어서 반환하는 일을 합니다.


그리고 formatHighLows() 메소드에서는 최고기온/최저기온 을 표시하도록 만듭니다.


getWeatherDataFromJson() 에서는 파싱하는 메소드인데요.


우선 날씨 기온 최고기온 최저기온 등 표시할 데이터의 제목들을 스트링 변후에 대입시킵니다.


그리고JSONObject 변수에 JSON 정보를 담습니다.

다음엔 이것을 배열 형식으로 변환시켜서 OWM_LIST 에 담습니다.


다음엔 현재 날짜, 시간을 설정하고, 현지시각을 사용하도록 만듭니다.


그리고 나서 for 문을 돌려서 아까 받았던 배열들을 처리합니다.


이 구문 안에서는 day, description, highANdLow 를 처리할 겁니다.


그리고 이 처리한 결과 값들을 처음 설정한 스트링에 담습니다.

day, description, highAndLow


그리고 for 문을 하나 더 만들어서 이 값을을 로그로 뿌리도록 합니다.


그리고 마지막으로 그 값을 반환합니다.


그 다음 메소드은 doInBackground() 는 눈에 익은 코드들이 보입니다.

지난 글에서 만들었던 코드들입니다.

날씨 싸이트에 요청을 보내고 이것을 JSON 형식으로 응답을 받는 겁니다.

이 작업은 백그라운드 쓰레드로 돌아가게 됩니다.


여기서는 우편번호를 파라미터로 받아서 사용합니다.


자세한 설명은 대부분 이전 글하고 중복 되게 되서 생략하겠습니다.


이 부분을 실행해서 로그를 살펴 보면 아래와 같은 형식으로 정보를 받는 걸 볼 수 있습니다.



08-28 12:33:24.293 1645-1836/? V/FetchWeatherTask: Forecast string: {"city":{"id":5043944,"name":"Rowland","coord":{"lon":-93.425507,"lat":44.860241},"country":"US","population":0},"cod":"200","message":0.0543,"cnt":7,"list":[{"dt":1472407200,"temp":{"day":27.02,"min":21.97,"max":28.3,"night":21.97,"eve":26.79,"morn":23.28},"pressure":1000.68,"humidity":81,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"02d"}],"speed":4.11,"deg":164,"clouds":8},{"dt":1472493600,"temp":{"day":29.55,"min":19.71,"max":30.15,"night":19.71,"eve":28.72,"morn":21.13},"pressure":1001.03,"humidity":77,"weather":[{"id":502,"main":"Rain","description":"heavy intensity rain","icon":"10d"}],"speed":5.26,"deg":203,"clouds":12,"rain":17.8},{"dt":1472580000,"temp":{"day":24.5,"min":15.42,"max":24.85,"night":15.42,"eve":22.39,"morn":18.77},"pressure":1001.02,"humidity":88,"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"speed":2.66,"deg":1,"clouds":32},{"dt":1472666400,"temp":{"day":23.55,"min":12.55,"max":23.55,"night":16.48,"eve":22.84,"morn":12.55},"pressure":1000.12,"humidity":0,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"speed":1.98,"deg":73,"clouds":1},{"dt":1472752800,"temp":{"day":22.07,"min":13.01,"max":22.07,"night":16.97,"eve":20.8,"morn":13.01},"pressure":997.94,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":4.83,"deg":151,"clouds":70,"rain":0.75},{"dt":1472839200,"temp":{"day":22.26,"min":16.52,"max":24.14,"night":22.41,"eve":24.14,"morn":16.52},"pressure":992.68,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":6.92,"deg":159,"clouds":64,"rain":0.92},{"dt":1472925600,"temp":{"day":27.25,"min":22.06,"max":27.76,"night":23.4,"eve":27.76,"morn":22.06},"pressure":985.79,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":3.1,"deg":228,"clouds":20,"rain":2.75}]}

여기에 어떤 정보가 있는지 보기 어려우면 JSON 관련 웹사이트에서 좀 알아보기 쉽도록 처리해서 볼 수 있습니다.


https://jsonformatter.curiousconcept.com/




이렇게 하면 받은 데이터 중 어떤 자료를 어떻게 처리 해야할 지 계획을 세울 때 좀 더 편리하게 이용할 수 있을 겁니다.


여기서는 main, max, min 같은 자료를 사용할 겁니다.


위에 코드가 있지만 JSON 데이터를 받는 방법은 아래와 같습니다.


JSONObject temperatureObject = dayForecast.getJSONObject(OWM_TEMPERATURE);



마지막으로 이 부분도 잘 봐야 하는데요.


@Override
        protected void onPostExecute(String[] result) {
            if (result != null) {
                mForecastAdapter.clear();
                for(String dayForecastStr : result) {
                    mForecastAdapter.add(dayForecastStr);
                }
                // New data is back from the server.  Hooray!
            }
        }


여기선 AsyckTask 의 작업이 다 끝난 후에 이것을 clear 해 주고 새로운 데이터를 뿌려주도록 합니다.

해당 작업이 끝난 후 UI 단에서 실행되는 메소드 입니다.




Refresh 버튼을 누른 후의 화면입니다.


제가 사는 지역의 일기예보인데요.


월요일이 좀 덥고 나머지는 지낼만 할 것 같습니다.


여기까지 2과 내용이었습니다.


아마 가장 기본적인 기능들은 구현 된 것 같습니다.


3과에서는 리스트 뷰에서 아이템을 누르면 그 아이템에 대한 좀 더 자세한 정보를 보여주는 기능을 구현할 겁니다.

저작자 표시 비영리 동일 조건 변경 허락
신고