728x90

mysql_query() expects parameter 2 to be resource, boolean given in


DB 데이터 업데이트 작업을 하는데 MySQLi 가 아닌 MySQL 로 하다보니 위와 같은 메시지가 나온다.


이런 메시지가 나오는 것은 MySQL 연결 정보가 제대로 연결되지 않아서다.


$db= mysql_connect("localhost","root","user_password");
mysql_select_db("dbname", $db);

블로그 이미지

Link2Me

,
728x90

https://developers.kakao.com/docs/android/user-management 에 나온 public class GlobalApplication extends Application {} 부분을 참조하여 코드를 구현한다.

 

테스트에 사용한 소스 코드는 맨 하단에 첨부했다.


GlobalApplication.java

package com.link2me.android.auth_kakao;

import android.app.Application;
import android.content.Context;

import com.kakao.auth.ApprovalType;
import com.kakao.auth.AuthType;
import com.kakao.auth.IApplicationConfig;
import com.kakao.auth.ISessionConfig;
import com.kakao.auth.KakaoAdapter;
import com.kakao.auth.KakaoSDK;

public class GlobalApplication extends Application {
    private static GlobalApplication instance;

    public static final GlobalApplication getGlobalApplicationContext() {
        if (instance == null)
            throw new IllegalStateException("this application does not inherit com.kakao.GlobalApplication");
        return instance;
    }

    protected static class KakaoSDKAdapter extends KakaoAdapter {
        /**
         * Session Config에 대해서는 default값들이 존재한다.
         * 필요한 상황에서만 override해서 사용하면 됨.
         *
         * @return Session의 설정값.
         */
        @Override
        public ISessionConfig getSessionConfig() {
            return new ISessionConfig() {
                @Override
                public AuthType[] getAuthTypes() {
                    return new AuthType[]{AuthType.KAKAO_LOGIN_ALL};
                }
                @Override
                public boolean isUsingWebviewTimer() {
                    return false;
                }
                @Override
                public boolean isSecureMode() {
                    return false;
                }
                @Override
                public ApprovalType getApprovalType() {
                    return ApprovalType.INDIVIDUAL;
                }
                @Override
                public boolean isSaveFormData() {
                    return true;
                }
            };
        }

        @Override
        public IApplicationConfig getApplicationConfig() {
            return new IApplicationConfig() {
                @Override
                public Context getApplicationContext() {
                    return GlobalApplication.getGlobalApplicationContext();
                }
            };
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
        KakaoSDK.init(new KakaoSDKAdapter());
    }

}


activity_kakao_login.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SampleLoginActivity">

    <com.kakao.usermgmt.LoginButton
        android:id="@+id/com_kakao_login"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginTop="250dp"
        android:layout_marginBottom="30dp"
        android:layout_weight="1"/>

</FrameLayout>



LoginActivity.java

package com.link2me.android.auth_kakao;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

import com.kakao.auth.ISessionCallback;
import com.kakao.auth.Session;
import com.kakao.util.exception.KakaoException;
import com.kakao.util.helper.log.Logger;

public class LoginActivity extends AppCompatActivity {
    static final String TAG = LoginActivity.class.getSimpleName();
    Context mContext;
    private SessionCallback callback;
    /**
     * 로그인 버튼을 클릭 했을시 access token을 요청하도록 설정한다.
     *
     * @param savedInstanceState 기존 session 정보가 저장된 객체
     */

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

        callback = new SessionCallback();
        Session.getCurrentSession().addCallback(callback);
        Session.getCurrentSession().checkAndImplicitOpen();
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (Session.getCurrentSession().handleActivityResult(requestCode, resultCode, data)) {
            return;
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Session.getCurrentSession().removeCallback(callback);
    }

    private class SessionCallback implements ISessionCallback {
        @Override
        public void onSessionOpened() {
            redirectSignupActivity();
        }
        @Override
        public void onSessionOpenFailed(KakaoException exception) {
            if (exception != null) {
                Logger.e(exception);
            }
        }
    }

    protected void redirectSignupActivity() {
        final Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
        finish();
    }

}


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_signup"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:layout_marginTop="50dp"
        android:textSize="14sp"
        android:text="SignupActivity" />

    <Button
        android:id="@+id/btn_logout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:layout_marginTop="20dp"
        android:text="Logout"/>

</LinearLayout>
 



MainActivity.java

package com.link2me.android.auth_kakao;

import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import com.kakao.network.ErrorResult;
import com.kakao.usermgmt.UserManagement;
import com.kakao.usermgmt.callback.LogoutResponseCallback;
import com.kakao.usermgmt.callback.MeV2ResponseCallback;
import com.kakao.usermgmt.callback.UnLinkResponseCallback;
import com.kakao.usermgmt.response.MeV2Response;
import com.kakao.util.helper.log.Logger;

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

public class MainActivity extends AppCompatActivity {
    static final String TAG = MainActivity.class.getSimpleName();

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

        TextView textView = findViewById(R.id.tv_signup);
        textView.setText("로그인 성공");
        requestMe();

        Button logout_btn = findViewById(R.id.btn_logout);
        logout_btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onClickLogout();
            }
        });
    }

    private void onClickLogout() {
        UserManagement.getInstance().requestLogout(new LogoutResponseCallback() {
            @Override
            public void onCompleteLogout() {
                redirectLoginActivity();
            }
        });
    }

    private void redirectLoginActivity() {
        final Intent intent = new Intent(this, LoginActivity.class);
        startActivity(intent);
        finish();
    }

    private void requestMe() {
        // https://developers.kakao.com/apps/361201/settings/user
        // 앱 생성된 이름 선택(테스트 앱) → 사용자 관리 → 접근권한관리항목 설정
        List<String> keys = new ArrayList<>();
        keys.add("properties.nickname");
        keys.add("properties.profile_image");
        keys.add("kakao_account.email");

        UserManagement.getInstance().me(keys, new MeV2ResponseCallback() {
            @Override
            public void onFailure(ErrorResult errorResult) {
                String message = "failed to get user info. msg=" + errorResult;
                Logger.d(message);
            }

            @Override
            public void onSessionClosed(ErrorResult errorResult) {
                redirectLoginActivity();
            }

            @Override
            public void onSuccess(MeV2Response response) {
                Log.e(TAG,"user id : " + response.getId());
                Log.e(TAG,"nickname : " + response.getNickname());
                Log.e(TAG,"email: " + response.getKakaoAccount().getEmail());
                //Logger.d("profile image: " + response.getKakaoAccount().getProfileImagePath());
                //redirectMainActivity();
            }

        });
    }

    private void onClickUnlink() {
        // 앱 연결 해제는 카카오 플랫폼에 연결된 사용자와 앱의 연결을 영구 해제함으로서 일반적으로 사용자가 앱 탈퇴 요청을 하는 경우와 비슷하다.
        // 앱 연결 해제가 수행된 사용자는 영구적으로 복구가 불가능하며 카카오 플랫폼 서비스를 더이상 사용할 수 없다.
        // 단, 다시 앱 연결을 통해 새로운 데이터로 카카오 플랫폼 서비스를 이용할 수는 있다.
        final String appendMessage = getString(R.string.com_kakao_confirm_unlink);
        new AlertDialog.Builder(this)
                .setMessage(appendMessage)
                .setPositiveButton(getString(R.string.com_kakao_ok_button),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                UserManagement.getInstance().requestUnlink(new UnLinkResponseCallback() {
                                    @Override
                                    public void onFailure(ErrorResult errorResult) {
                                        Logger.e(errorResult.toString());
                                    }

                                    @Override
                                    public void onSessionClosed(ErrorResult errorResult) {
                                        redirectLoginActivity();
                                    }

                                    @Override
                                    public void onNotSignedUp() {
                                        LoginActivity loginActivity = new LoginActivity();
                                        loginActivity.redirectSignupActivity();
                                    }

                                    @Override
                                    public void onSuccess(Long userId) {
                                        redirectLoginActivity();
                                    }
                                });
                                dialog.dismiss();
                            }
                        })
                .setNegativeButton(getString(R.string.com_kakao_cancel_button),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.dismiss();
                            }
                        }).show();

    }

}



소스코드

- src/main/res/values/strings.xml 을 수정한다.

<string name="kakao_app_key">본인의 앱 키를 적으세요</string>


auth_kakao.zip


블로그 이미지

Link2Me

,
728x90

https://developers.kakao.com/ 에 접속하면 연동에 필요한 기본 사항이 설명되어 있다.

 

스크롤해서 내리면 Gradle 환경에서 사용하기 부분이 나온다.

 

 

프로젝트 build.gradle 에 추가할 사항

allprojects {
    repositories {
        google()
        jcenter()
        //kakao
        mavenCentral()
        maven { url 'http://devrepo.kakao.com:8088/nexus/content/groups/public/' }
    }
}

 

앱 build.gradel 에 추가할 사항

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation group: 'com.kakao.sdk', name: 'usermgmt', version: '1.14.0'
}
 

 

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.link2me.android.auth_kakao">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:name=".GlobalApplication"
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".SampleLoginActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <meta-data
            android:name="com.kakao.sdk.AppKey"
            android:value="@string/kakao_app_key" />

        <activity
            android:name="com.kakao.auth.authorization.authcode.KakaoWebViewActivity"
            android:launchMode="singleTop"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>
 

 

 

앱 생성

1. 내 애플리케이션 > 앱 만들기를 통해 앱을 생성한다.

   - 테스트 앱으로 생성했다.

 

2. Android SDK 사용시 네이티브 앱 키

   - 생성한 앱의 res/values/strings.xml 파일 내에 키를 복사하여 붙여넣기 한다.

<resources>
    <string name="app_name">Auth_KaKaO</string>
    <string name="kakao_app_key">1e6fd36c1b12AAA2dc22346c3544516f</string>
</resources>

 

3. 플랫폼 추가

   - 플랫폼 추가를 눌러서 패키지명을 등록한다.

 

 

키 해시를 등록해야 하는데

Commandline으로 키해시 구하기 로 했더니 openssl 에 대한 에러 메시지가 나오더라.

그래서 앱 내 자바 코드로 키 해시 구하기 메소드를 이용해서 구했다.

 

import android.content.pm.Signature;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;

import androidx.appcompat.app.AppCompatActivity;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import static com.kakao.util.helper.Utility.getPackageInfo;

public class GetHashKey extends AppCompatActivity {
    static final String TAG = GetHashKey.class.getSimpleName();

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

        System.out.println("key Hash : " +  getKeyHash(GetHashKey.this));
    }

    public static String getKeyHash(final Context context) {
        PackageInfo packageInfo = getPackageInfo(context, PackageManager.GET_SIGNATURES);
        if (packageInfo == null)
            return null;

        for (Signature signature : packageInfo.signatures) {
            try {
                MessageDigest md = MessageDigest.getInstance("SHA");
                md.update(signature.toByteArray());
                return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
            } catch (NoSuchAlgorithmException e) {
                Log.w(TAG, "Unable to get MessageDigest. signature=" + signature, e);
            }
        }
        return null;
    }
}

 

블로그 이미지

Link2Me

,
728x90

GridCellAdapter 대신에 RecyclerView 기반 CalendarAdapter를 만들면 어떤 사항들이 변경되는지 알아보자.


RecyclerView 는 ListView 보다 메모리, 성능이 개선된 것으로 데이터 양이 많은 경우 스크롤을 효율적으로 수행할 수 있는 위젯이다.


앱 build.gradle 수정사항

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'androidx.multidex:multidex:2.0.0' // minSdkVersion 이 21 이하인 경우 사용
    implementation files('libs/icu4j-4_8_2.jar')
}



activity_main.xml

<GridView
    android:id="@+id/calendar"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:scrollbars="horizontal"
    android:numColumns="7" >
</GridView>

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/calendar"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_centerVertical="true"
    android:adjustViewBounds="false"
    android:focusableInTouchMode="true"
    android:scrollbars="vertical" />


MainActivity.java

    private GridView calendarView;
    private GridCellAdapter adapter;


        calendarView = (GridView) this.findViewById(R.id.calendar);

        makeCalendarList(month, year);

        // Initialised
        adapter = new GridCellAdapter(mContext, calList);
        adapter.notifyDataSetChanged();
        calendarView.setAdapter(adapter);
    }

    private void setGridCellAdapterToDate(int month, int year) {
        makeCalendarList(month, year);
        adapter = new GridCellAdapter(mContext, calList);
        adapter.notifyDataSetChanged();
        calendarView.setAdapter(adapter);
        // 상단 화면 날짜 출력
        cal.set(year, month -1, cal.get(Calendar.DAY_OF_MONTH));
        currentMonth.setText(DateFormat.format(dateTemplate, cal.getTime()));
    }

    private RecyclerView calendarView;
    private RecyclerView.Adapter adapter;

        calendarView = this.findViewById(R.id.calendar);
        calendarView.setHasFixedSize(true);
        calendarView.setLayoutManager(new GridLayoutManager(mContext,7));

        makeCalendarList(month, year);

        // Initialised
        adapter = new CalendarAdapter(mContext, calList);
        adapter.notifyDataSetChanged();
        calendarView.setAdapter(adapter);
    }

    private void setGridCellAdapterToDate(int month, int year) {
        makeCalendarList(month, year);
        adapter = new CalendarAdapter(mContext, calList);
        adapter.notifyDataSetChanged();
        calendarView.setAdapter(adapter);
        // 상단 화면 날짜 출력
        cal.set(year, month -1, cal.get(Calendar.DAY_OF_MONTH));
        currentMonth.setText(DateFormat.format(dateTemplate, cal.getTime()));
    }


이제 CalendarAdapter 만드는 방법이다.

public class CalendarAdapter extends RecyclerView.Adapter<CalendarAdapter.CustomViewHolder> {

    @NonNull
    @Override
    public CalendarAdapter.CustomViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return null;
    }

    @Override
    public void onBindViewHolder(@NonNull CalendarAdapter.CustomViewHolder holder, int position) {

    }

    @Override
    public int getItemCount() {
        return 0;
    }

    public class CustomViewHolder extends RecyclerView.ViewHolder {
        public CustomViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }
} 


아래 이미지와 같은 과정을 거치면 위와 같은 코드가 만들어진다.








이제 완성된 코드를 적는다.

package com.link2me.android.adpater;

import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.link2me.android.calendar.R;
import com.link2me.android.item.Calendar_Item;
import com.link2me.android.util.CalendarHelper;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;

public class CalendarAdapter extends RecyclerView.Adapter<CalendarAdapter.CustomViewHolder> {
    static final String tag = CalendarAdapter.class.getSimpleName();
    Context mContext;

    private LinkedHashMap<String, Calendar_Item> calList;
    private String[] mKeys;

    private final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd");

    public CalendarAdapter(Context context, LinkedHashMap<String, Calendar_Item> list) {
        mContext = context;
        calList = list;
        mKeys = calList.keySet().toArray(new String[list.size()]);
    }

    @NonNull
    @Override
    public CalendarAdapter.CustomViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.calendar_cell, parent, false);
        CustomViewHolder holder = new CustomViewHolder(view);
        return holder;
    }

    @Override
    public void onBindViewHolder(@NonNull final CalendarAdapter.CustomViewHolder holder, int position) {
        String theday = calList.get(mKeys[position]).getDay();
        String themonth = calList.get(mKeys[position]).getMonth();
        String theyear = calList.get(mKeys[position]).getYear();
        String holiday = calList.get(mKeys[position]).getEvent();

        holder.gridcell_Day.setText(theday);
        holder.gridcell_LunarDay.setText(CalendarHelper.Sol2Lun(theyear, String.valueOf(Integer.parseInt(themonth)-1),theday)); //0 ~ 11월로 인식하므로 - 1
        if(holiday.length()>0){
            holder.gridcell_Event.setText(holiday);
        } else {
            holder.gridcell_Event.setText("");
        }
        holder.gridcell_layout.setTag(theday + "-" + themonth + "-" + theyear);

        if(calList.get(mKeys[position]).getColor().equals("GRAY")){
            holder.gridcell_Day.setTextColor(Color.LTGRAY);
        }
        if(calList.get(mKeys[position]).getColor().equals("BLACK")){
            holder.gridcell_Day.setTextColor(Color.BLACK);
        }
        if(calList.get(mKeys[position]).getColor().equals("RED")){
            holder.gridcell_Day.setTextColor(Color.RED);
            holder.gridcell_Event.setTextColor(Color.RED);
        }
        if(calList.get(mKeys[position]).getColor().equals("BLUE")){
            holder.gridcell_Day.setTextColor(Color.BLUE);
        }
        if(calList.get(mKeys[position]).getColor().equals("CYAN")){
            holder.gridcell_layout.setBackgroundColor(Color.CYAN);
        }

        String tagData = theyear+themonth+theday;
        holder.itemView.setTag(tagData);
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String date_month_year = (String) view.getTag();
                Log.e("Selected date", date_month_year);
                try {
                    Date parsedDate = dateFormatter.parse(date_month_year);
                    Log.d(tag, "Parsed Date: " + parsedDate.toString());

                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return calList.size();
    }

    public class CustomViewHolder extends RecyclerView.ViewHolder {
        protected LinearLayout gridcell_layout;
        protected TextView gridcell_Day;
        protected TextView gridcell_LunarDay;
        protected TextView gridcell_Event;

        public CustomViewHolder(@NonNull View itemView) {
            super(itemView);
            gridcell_layout = (LinearLayout) itemView.findViewById(R.id.calendar_day_gridcell);
            gridcell_Day = itemView.findViewById(R.id.dayTV);
            gridcell_LunarDay = itemView.findViewById(R.id.lunardayTV);
            gridcell_Event = itemView.findViewById(R.id.eventTV);
        }
    }
}
 


커스텀 달력 만드는 방법에 대한 충분한 설명이 되었을 것이라고 본다.


https://www.youtube.com/watch?v=kNq9w1_nhL4 를 강좌를 보면 RecyclerView 에 대한 설명을 잘 해주고 있으니 사용법에 대해 잘 모르면 보면 도움될 수 있다.

블로그 이미지

Link2Me

,
728x90

RecyclerView: No layout manager attached; skipping layout

라는 로그를 출력하고 있다.

무엇이 문제일까?


calendarView = this.findViewById(R.id.calendar);

바로 밑에다가


calendarView.setHasFixedSize(true);
calendarView.setLayoutManager(new GridLayoutManager(mContext,7));

두줄을 추가해주면 해결된다.


블로그 이미지

Link2Me

,
728x90

안드로이드 AlarmManager 를 이용한 알람 예제를 테스트 하고 적어둔다.

서비스 개념을 이해하는데에는 도움이 되지만 완벽한 앱 알람 기능을 해결해주진 않는거 같다.

Job Scheduler 로 기능을 구현해 봐야 하나 싶은건 아닌가 싶기도 한데 아직 안해봐서 ....


출처는 https://sh-itstory.tistory.com/64 에서 제공한 https://github.com/aiex1234/PleaseWakeMeUp 에서 파일을 다운로드 받았다. 코드 덕분에 좀 더 편하게 활용 테스트가 가능했다.


삼성 갤럭시 S10(Android 9) 와 LG G5(Android 6.01) 두가지 폰에서 테스트했고 여타 자료를 참조하여 코드를 보강하였다.

알람을 설정한 시간이 되면 음악이 자동 재생되고, Stop 버튼을 누르면 재생이 멈추는 기능은 완벽하게 동작한다.

서비스에 대한 개념 이해는 https://link2me.tistory.com/1343 참조하면 된다.


Notification 메시지 보내는 것은 https://link2me.tistory.com/1514 에 올려진 파일을 받아서 androidX 로 변환해서 사용하면 100% 동작한다.

알람메시지를 받으면 띄워줄 Activity 가 MainActivity 이므로 MainActivity.class 로 수정하고, MainActivity.java 파일내에

        NotificationHelper notificationHelper = new NotificationHelper(mContext);
        notificationHelper.cancelNotification(mContext,0);
를 추가해주면 어플이 실행되고 있지 않아도 지정된 시간이 되면 Broadcast 메시지가 동작하여 음악을 재생하고 Notification 을 보내준다.

누르면 MainActivity 화면이 나오고, 종료 버튼을 누르면 재생되던 음악이 종료된다.


날짜를 지정하고, 시간을 설정하는 것까지 추가하면 알람 앱 기본 기능으론 충분하다고 본다.


앱 build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28

    defaultConfig {
        applicationId "com.link2me.android.wakemeup"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
}


기본적으로 디바이스를 종료하면 모든 알람은 취소된다.

이러한 상황이 벌어지는 것을 막기 위해, 유저가 디바이스를 재부팅했을 때 앱이 자동적으로 알람을 재구동 하도록 설계할 수 있다.

앱 매니페스트에 RECEIVE_BOOT_COMPLETED 권한을 설정한다. 이 액션은 시스템 부팅 완료 후 브로드캐스트인 ACTION_BOOT_COMPLETED 을 받을 수 있도록 해준다.

이와 관련된 코드 구현은 하지 않았다.


AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.link2me.android.wakemeup">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/icon_watch"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <service
            android:name=".RingtoneService"
            android:enabled="true"></service>

    </application>

</manifest>


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:gravity="top">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TimePicker
            android:id="@+id/time_picker"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center">

        <Button
            android:layout_width="60dp"
            android:layout_height="50dp"
            android:id="@+id/btn_alarmStart"
            android:text="시작"/>

        <Button
            android:layout_width="60dp"
            android:layout_height="50dp"
            android:id="@+id/btn_alarmFinish"
            android:text="종료" />

    </LinearLayout>

    <TextView
        android:id="@+id/tv_alarmON"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:layout_marginTop="30dp"
        android:text="알람 예정 시간"
        android:textSize="14dp"
        android:textColor="@color/colorAccent"/>

</LinearLayout>


MainActivity.java

package com.link2me.android.wakemeup;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.TimePicker;
import android.widget.Toast;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;

import com.link2me.android.common.BackPressHandler;

import java.util.Calendar;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();
    Context mContext;
    TextView textView;
    TimePicker alarm_timepicker;
    AlarmManager alarm_manager;
    String am_pm;
    int getHourTimePicker = 0;
    int getMinuteTimePicker = 0;

    Intent alarmIntent;
    PendingIntent pendingIntent;
    private static final int REQUEST_CODE = 1111;
    SharedPreferences pref;

    private BackPressHandler backPressHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = MainActivity.this;
        backPressHandler = new BackPressHandler(this); // 뒤로 가기 버튼 이벤트

        textView = findViewById(R.id.tv_alarmON);
        textView.setText("");

        // 알람매니저 설정
        alarm_manager = (AlarmManager) mContext.getSystemService(ALARM_SERVICE);
        // 알람매니저가 유용한 이유는, Alarm이 한번 등록되면 어플리케이션의 생명주기와 관계없이
        // 어플리케이션이 종료되어있는 경우에도 지정해놓은 operation에 대해 어김없이 실행할 수 있다는 것

        // 타임피커 설정
        alarm_timepicker = findViewById(R.id.time_picker);
        alarm_timepicker.setIs24HourView(true);

        // 알람 Receiver 인텐트 생성
        alarmIntent = new Intent(mContext, AlarmReceiver.class);

        // Button Alarm ON
        Button alarm_on = findViewById(R.id.btn_alarmStart);
        alarm_on.setOnClickListener(new View.OnClickListener() {
            @RequiresApi(api = Build.VERSION_CODES.M)
            @Override
            public void onClick(View v) {
                setAlarm(mContext);
            }
        });

        Button alarm_off = findViewById(R.id.btn_alarmFinish);
        alarm_off.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                releaseAlarm(mContext);
            }
        });
    }

    private void setAlarm(Context context){
        // Calendar 객체 생성
        final Calendar calendar = Calendar.getInstance();

        // calendar에 시간 셋팅
        if (Build.VERSION.SDK_INT < 23) {
            // 시간 가져옴
            getHourTimePicker = alarm_timepicker.getCurrentHour();
            getMinuteTimePicker = alarm_timepicker.getCurrentMinute();
        } else {
            // 시간 가져옴
            getHourTimePicker = alarm_timepicker.getHour();
            getMinuteTimePicker = alarm_timepicker.getMinute();
        }

        // 현재 지정된 시간으로 알람 시간 설정
        calendar.setTimeInMillis(System.currentTimeMillis());
        calendar.set(Calendar.HOUR_OF_DAY, getHourTimePicker);
        calendar.set(Calendar.MINUTE, getMinuteTimePicker);
        calendar.set(Calendar.SECOND, 0);

        pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.putInt("set_hour", getHourTimePicker);
        editor.putInt("set_min", getMinuteTimePicker);
        editor.putString("state", "ALARM_ON");
        editor.commit();

        // reveiver에 string 값 넘겨주기
        alarmIntent.putExtra("state","ALARM_ON");

        // receiver를 동작하게 하기 위해 PendingIntent의 인스턴스를 생성할 때, getBroadcast 라는 메소드를 사용
        // requestCode는 나중에 Alarm을 해제 할때 어떤 Alarm을 해제할지를 식별하는 코드
        pendingIntent = PendingIntent.getBroadcast(mContext,REQUEST_CODE,alarmIntent,PendingIntent.FLAG_UPDATE_CURRENT);

        long currentTime = System.currentTimeMillis(); // 현재 시간
        //long triggerTime = SystemClock.elapsedRealtime() + 1000*60;
        long triggerTime = calendar.getTimeInMillis(); // 알람을 울릴 시간
        long interval = 1000 * 60 * 60  * 24; // 하루의 시간

        while(currentTime > triggerTime){ // 현재 시간보다 작다면
            triggerTime += interval; // 다음날 울리도록 처리
        }
        Log.e(TAG, "set Alarm : " + getHourTimePicker + " 시 " + getMinuteTimePicker + "분");

        // 알림 세팅 : AlarmManager 인스턴스에서 set 메소드를 실행시키는 것은 단발성 Alarm을 생성하는 것
        // RTC_WAKEUP : UTC 표준시간을 기준으로 하는 명시적인 시간에 intent를 발생, 장치를 깨움
        if (Build.VERSION.SDK_INT < 23) {
            if (Build.VERSION.SDK_INT >= 19) {
                alarm_manager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
            } else {
                // 알람셋팅
                alarm_manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
            }
        } else {  // 23 이상
            alarm_manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
            //alarm_manager.set(AlarmManager.RTC_WAKEUP, triggerTime,pendingIntent);
            //알람 매니저를 통한 반복알람 설정
            //alarm_manager.setRepeating(AlarmManager.RTC, triggerTime, interval, pendingIntent);
            // interval : 다음 알람이 울리기까지의 시간
        }

        // Unable to find keycodes for AM and PM.
        if(getHourTimePicker > 12){
            am_pm = "오후";
            getHourTimePicker = getHourTimePicker - 12;
        } else {
            am_pm ="오전";
        }
        textView.setText("알람 예정 시간 : "+ am_pm +" "+ getHourTimePicker + "시 " + getMinuteTimePicker + "분");
    }

    public void releaseAlarm(Context context)  {
        Log.e(TAG, "unregisterAlarm");

        pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.putString("state", "ALARM_OFF");
        editor.commit();

        // 알람매니저 취소
        alarm_manager.cancel(pendingIntent);
        alarmIntent.putExtra("state","ALARM_OFF");

        // 알람 취소
        sendBroadcast(alarmIntent);

        Toast.makeText(MainActivity.this,"Alarm 종료",Toast.LENGTH_SHORT).show();
        textView.setText("");
    }

    @Override
    public void onBackPressed() {
        backPressHandler.onBackPressed();
    }

}


AlarmReceiver.java

package com.link2me.android.wakemeup;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.MediaPlayer;
import android.os.PowerManager;
import android.util.Log;

public class AlarmReceiver extends BroadcastReceiver{
    private static final String TAG = AlarmReceiver.class.getSimpleName();
    Context mContext;

    PowerManager powerManager;
    private static PowerManager.WakeLock wakeLock;
    SharedPreferences pref;
    String get_state;
    private MediaPlayer mediaPlayer;

    @Override
    public void onReceive(Context context, Intent intent) {
        mContext = context;
        pref = context.getSharedPreferences("pref", Activity.MODE_PRIVATE);
        get_state = pref.getString("state","");
        Log.e(TAG, "Alarm state : " + get_state);

        AlarmReceiverChk(context, intent);
    }

    private void AlarmReceiverChk(final Context context, final Intent intent){
        Log.d(TAG, "Alarm Receiver started!");
        switch (get_state){
            case "ALARM_ON":
                acquireCPUWakeLock(context, intent);
                // RingtoneService 서비스 intent 생성
                Intent serviceIntent = new Intent(mContext, RingtoneService.class);
                serviceIntent.putExtra("state", get_state);
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){
                    context.startForegroundService(serviceIntent);
                } else {
                    context.startService(serviceIntent);
                }
                break;
            case "ALARM_OFF": // stopService 가 동작하지 않아서 startService 로 처리하고 나서....
                releaseCpuLock();
                Intent stopIntent = new Intent(context, RingtoneService.class);
                stopIntent.putExtra("state", get_state);
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){
                    context.startForegroundService(stopIntent);
                } else {
                    context.startService(stopIntent);
                }
                break;
        }
    }

    @SuppressLint("InvalidWakeLockTag")
    private void acquireCPUWakeLock(Context context, Intent intent) {
        // 잠든 화면 깨우기
        if (wakeLock != null) {
            return;
        }
        powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP
                | PowerManager.ON_AFTER_RELEASE, "WAKELOCK");
        wakeLock.acquire();
        Log.e("PushWakeLock", "Acquire cpu WakeLock = " + wakeLock);
    }

    private void releaseCpuLock() {
        Log.e("PushWakeLock", "Releasing cpu WakeLock = " + wakeLock);

        if (wakeLock != null) {
            wakeLock.release();
            wakeLock = null;
        }
    }
}


RingtoneService.java

package com.link2me.android.wakemeup;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;


public class RingtoneService extends Service{
    private static final String TAG = RingtoneService.class.getSimpleName();
    MediaPlayer mediaPlayer;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        // Service 객체와 (화면단 Activity 사이에서) 데이터를 주고받을 때 사용하는 메서드
        // 데이터를 전달할 필요가 없으면 return null;
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 서비스에서 가장 먼저 호출됨(최초에 한번만)
        Log.i(TAG, "RingtoneService Started");

        NotificationHelper notificationHelper = new NotificationHelper(getApplicationContext());
        notificationHelper.createNotification("알람시작","알람음이 재생됩니다.");

        // https://link2me.tistory.com/1514 에 첨부된 파일 받아서 수정 사용하면 해결됨

    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand startID === "+startId); // 계속 증가되는 값

        String getState = intent.getExtras().getString("state");
        Log.e("TAG","onStartCommand getState : " + getState);

        switch (getState) {
            case "ALARM_ON":
                if(mediaPlayer == null){
                    mediaPlayer = MediaPlayer.create(this,R.raw.ouu);
                    mediaPlayer.start();

                    mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                        @Override
                        public void onCompletion(MediaPlayer mp) {
                            Log.e(TAG, "mediaPlayer Completed!");
                            mediaPlayer.stop();
                            mediaPlayer.reset();
                            mediaPlayer.release();
                        }
                    });
                }
                break;
            case "ALARM_OFF":
                Log.e(TAG, "onStartCommand Stoped!");

                if (mediaPlayer != null) {
                    if(mediaPlayer.isPlaying() == true){
                        mediaPlayer.stop();
                        mediaPlayer.release(); // 자원 반환
                        mediaPlayer = null;
                    }
                }
                stopSelf();
                break;
            default:
                break;
        }
        return START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        Log.i(TAG, "Action Service Ended");
        super.onDestroy();
        stopForeground(true);
    }
}



도움이 되셨다면 ... 해 주세요. 좋은 글 작성에 큰 힘이 됩니다.

me.tistory.com/1110 [소소한 일상 및 업무TIP 다루기]


블로그 이미지

Link2Me

,
728x90

이번에는 MainActivity.java 에서

ArrayList<HashMap<String, String>> calList = new ArrayList<HashMap<String, String>>();

LinkedHashMap<String, Calendar_Item> calList = new LinkedHashMap<String, Calendar_Item>();

로 변경하면 어떤 것들을 수정해줘야 할까?


먼저 https://link2me.tistory.com/1717 게시글을 읽고 나서 아래 비교 내용을 보면 좀 더 이해하는데 도움이 될 것으로 본다.

HashMap 을 사용하면 입력한 순서대로 출력이 될 것을 기대하지만 뒤죽박죽으로 결과를 보여준다.


MainActivity.java 수정사항

                if(CurrentMonth-1 == thisMonth){ // 현재월이면
                    if(i == thisDay){
                        Log.e(TAG, "key := " + key);
                        int index = getIndexOfCalList(key);
                        HashMap<String, String> item = calendarItem(String.valueOf(yy),String.valueOf(CurrentMonth),String.valueOf(i),weekday,"CYAN","",key);
                        calList.set(index,item);
                    }
                }


    private HashMap<String, String> calendarItem(String year, String month, String day, int weekday, String color, String name, String key){
        HashMap<String, String> map = new HashMap<String, String>();
        map.put("year",year);
        map.put("month",month);
        map.put("day",day);
        map.put("weekday", String.valueOf(weekday));
        map.put("color",color);
        map.put("event",name);
        map.put("key",key);
        return map;
    }

    private void addCalendarItem(String year, String month, String day, int weekday, String color,String name, String key){
        HashMap<String, String> item = calendarItem(year,month,day,weekday,color,name,key);
        calList.add(item);
    }

    private int getIndexOfCalList(String search_key) {
        for (int temp = 0; temp < calList.size(); temp++) {
            String key = calList.get(temp).get("key");
            if (key != null && key.equals(search_key)) {
                return temp;
            }
        }
        return -1;
    }

                if(CurrentMonth-1 == thisMonth){ // 현재월이면
                    if(i == thisDay){
                        Log.e(TAG, "key := " + key);
                        addCalendarItem(String.valueOf(yy),String.valueOf(CurrentMonth),String.valueOf(i),weekday,"CYAN","",key);
                    }
                }

    private Calendar_Item calendarItem(String year, String month, String day, int weekday, String color, String name, String key){
        Calendar_Item item = new Calendar_Item();
        item.setYear(year);
        item.setMonth(month);
        item.setDay(day);
        item.setWeekday(weekday);
        item.setColor(color);
        item.setEvent(name);
        return item;
    }

    private void addCalendarItem(String year, String month, String day, int weekday, String color,String name, String key){
        Calendar_Item item = calendarItem(year,month,day,weekday,color,name,key);
        calList.put(key,item);
    }


GridCellAdapter.java 수정사항

    private final ArrayList<HashMap<String, String>> calList;

    public GridCellAdapter(Context context, ArrayList<HashMap<String, String>> list) {
        mContext = context;
        calList = list;
    }

    public HashMap<String, String> getItem(int position) {
        return calList.get(position);
    }

        String theday = calList.get(position).get("day");
        int themonth = Integer.parseInt(calList.get(position).get("month"));
        String theyear = calList.get(position).get("year");
        String holiday = calList.get(position).get("event");

    private final LinkedHashMap<String, Calendar_Item> calList;
    private String[] mKeys;

    public GridCellAdapter(Context context, LinkedHashMap<String, Calendar_Item> list) {
        mContext = context;
        calList = list;
        mKeys = calList.keySet().toArray(new String[list.size()]);
    }

    public Calendar_Item getItem(int position) {
        return calList.get(mKeys[position]);
    }

        String theday = calList.get(mKeys[position]).getDay();
        int themonth = Integer.parseInt(calList.get(mKeys[position]).getMonth());
        String theyear = calList.get(mKeys[position]).getYear();
        String holiday = calList.get(mKeys[position]).getEvent();


어떤 것을 사용하든지 검색 속도가 느리지 않으면서 확장성을 고려한 메소드를 적절하게 사용하면 된다.


공휴일 등록, 기념일 등록, 일정 등록 등의 루틴은 여기에는 적지 않았다.


도움이 되셨다면 ... 해 주세요. 좋은 글 작성에 큰 힘이 됩니다.

블로그 이미지

Link2Me

,
728x90

HashMap ketSet<String> 을 String 배열로 만들는 법을 적어둔다.

String[] mKeys = map.keySet().toArray(new String[map.size()]);

를 활용할 기회가 있었다.

key 로부터 map.get(key); 로 값을 얻을 수 있다.


key 를 배열로 만들어야 value를 얻을 수 있을 때 아래 예제를 살펴보면 도움이 될 것이다.


import java.util.HashMap;
import java.util.Map;

public class HashMap_Test {

    public static void main(String[] args) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("1", "대조영");
        map.put("2", "이순신");
        map.put("4", "강감찬");
        map.put("3", "양만춘");
        map.put("5", "을지문덕");
        map.put("3", "연개소문"); // HashMap 은 key 가 같고 값이 다르면 value를 덮어쓰기를 한다.
       
        // Getting the value of "4" key.
        System.out.println("The Value is: " + map.get("4"));

        map.containsKey("강아지"); // key 가 존재하면 true 반환
        map.containsValue("조나비"); // value 가 존재하면 true 반환

        // Using keySet() to get the set view of keys
        System.out.println("The set is: " + map.keySet());
       
        String[] mKeys = map.keySet().toArray(new String[map.size()]);
        for (String key : mKeys) {
            System.out.println(key);
        }
       
        for(int i=0; i < mKeys.length;i++) {
            System.out.println("key :" + mKeys[i] + ", value : " + map.get(mKeys[i]));
        }
    }

}
 


HashMap의 경우 단점이 put을 통해 데이터나 객체를 넣을때 key의 순서가 지켜지지 않는다는 것이다.
개발을 할때 코드상으로 순차적으로 key/value를 넣어도, 실제 HashMap에서는 해당 순서가 지켜지지 않는다.
만약 입력된 Key의 순서가 보장되어야 한다면 LinkedHashMap을 사용하면 된다.

LinkedHashMap 은 기본적으로 HashMap을 상속받아 만들어져 있게 때문에 HashMap의 기능을 그대로 사용 가능하다.
대신 여기에 순서라는 개념이 들어 갔다.



참고하면 도움이 될 자료

https://stackoverflow.com/questions/5234576/what-adapter-shall-i-use-to-use-hashmap-in-a-listview


블로그 이미지

Link2Me

,
728x90

Hashmap 을 ArrayList 로 변환하는 방법에 대한 코드 예제가 있어서 약간 추가해서 테스트해보고 적어둔다.

HashMap 은 Map 인터페이스의 한 종류로 key 와 value 를 묶어서 하나의 entry로 저장한다는 특징을 가지고 있다.

key 값은 중복이 불가능하고 value 값은 중복이 가능하며, null 값도 사용 가능하다.

해싱 기법으로 데이터를 저장하고, 데이터가 많아도 검색이 빠르다.

멀티쓰레드에서 동시에 HashMap key - value 를 사용하면 문제가 될 수 있다.

멀티쓰레드에서는 HashTable을 쓴다.


HashMap 을 ArrayList 로 변환하는 3가지 방법이 있다.

HashMap key 를 ArrayList 로 변환, HashMap value 를 ArrayList 로 변환, HashMap key-value 를 ArrayList 로 변환할 수 있다.

Iterator 인터페이스를 사용할 수 없는 컬렉션인 Map 에서 Iterator 인터페이스를 사용하기 위해서는 Map 에 keySet(), entrySet() 메소드를 사용하여 Set 객체를 반환한 후 Iterator 인터페이스를 사용하면 된다.


import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Set;

public class HashMapToArrayList {

    public static void main(String[] args) {
        //Creating a HashMap object
        HashMap<String, String> map = new HashMap<String, String>();

        //Adding elements to HashMap
        map.put("1", "이순신");
        map.put("2", "강감찬");
        map.put("5", "주몽");
        map.put("6", "대조영");
        map.put("3", "을지문덕");
        map.put("4", "연개소문");

        // Getting Set of keys
        Set<String> keySet = map.keySet();

        // ArrayList 의 key 생성
        ArrayList<String> listOfKeys = new ArrayList<String>(keySet);
        System.out.println("ArrayList Of Keys :");
        for (String key : listOfKeys) {
            System.out.println(key);
        }
        System.out.println("--------------------------");

        // Getting Collection of values
        Collection<String> values = map.values();

        // ArrayList 의 값 생성
        ArrayList<String> listOfValues = new ArrayList<String>(values);
        System.out.println("ArrayList Of Values :");
        for (String value : listOfValues) {
            System.out.println(value);
        }
        System.out.println("--------------------------");

        //Getting the Set of entries
        Set<Entry<String, String>> entrySet = map.entrySet();

        // ArrayList 의  Entry objects 생성
        ArrayList<Entry<String, String>> listOfEntry = new ArrayList<Entry<String,String>>(entrySet);
        System.out.println("ArrayList of Key-Values :");
        for (Entry<String, String> entry : listOfEntry) {
            System.out.println(entry.getKey()+" : "+entry.getValue());
        }


        // entrySet().iterator()
        Iterator<Map.Entry<String,String>> iteratorE = map.entrySet().iterator();
        System.out.println("------- Iterator<Map.Entry<String,String>> -----------");
        while(iteratorE.hasNext()) {
            Map.Entry<String, String> entry = (Map.Entry<String, String>) iteratorE.next();
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println("key : " + key + " , value : "+value);
        }
       
        // 방법 05 : keySet().iterator()
        Iterator<String> iteratorK = map.keySet().iterator();
        System.out.println("------- Iterator<String> iteratorK -----------------");
        while (iteratorK.hasNext()) {
            String key = iteratorK.next();
            String value = map.get(key);
            System.out.println("key : " + key + ", value : " + value);
        }

    }
}


참고하면 도움이 될 자료

ArrayList and HashMap : https://link2me.tistory.com/1210

https://javaconceptoftheday.com/convert-hashmap-to-arraylist-in-java/


Map 동영상 강좌

https://programmers.co.kr/learn/courses/9/lessons/260#

Java의 정석, 11. 컬렉션 프레임웍 : https://www.youtube.com/watch?v=y3qLfTOmONE


블로그 이미지

Link2Me

,
728x90

안드로이드 달력 만들기 MainActivity.java 와 GridCellAdapter.java 파일에서 아래와 같이 ArrayList 제네릭으로 HashMap<String, String> 을 사용하는 방법이다.


ArrayList<Calendar_Item> calList = new ArrayList<>();

ArrayList<HashMap<String, String>> calList = new ArrayList<>();

로 변경하여 코딩하면 어떻게 처리해야 할까?


Calendar_Item 이 HashMap<String, String> 라는 것으로 변경된 것만 살펴보고 수정해주면 된다.


    private Calendar_Item calendarItem(String year, String month, String day, int weekday, String color, String name, String key){
        Calendar_Item item = new Calendar_Item();
        item.setYear(year);
        item.setMonth(month);
        item.setDay(day);
        item.setWeekday(weekday);
        item.setColor(color);
        item.setEvent(name);
        item.setKey(key);
        return item;
    }

    private void addCalendarItem(String year, String month, String day, int weekday, String color,String name, String key){
        Calendar_Item item = calendarItem(year,month,day,weekday,color,name,key);
        calList.add(item);
    }

    private int getIndexOfCalList(String search_key) {
        for (int temp = 0; temp < calList.size(); temp++) {
            String key = calList.get(temp).getKey();
            if (key != null && key.equals(search_key)) {
                return temp;
            }
        }
        return -1;
    }
 

    private HashMap<String, String> calendarItem(String year, String month, String day, int weekday, String color, String name, String key){
        HashMap<String, String> map = new HashMap<String, String>();
        map.put("year",year);
        map.put("month",month);
        map.put("day",day);
        map.put("weekday", String.valueOf(weekday));
        map.put("color",color);
        map.put("event",name);
        map.put("key",key);
        return map;
    }

    private void addCalendarItem(String year, String month, String day, int weekday, String color,String name, String key){
        HashMap<String, String> item = calendarItem(year,month,day,weekday,color,name,key);
        calList.add(item);
    }

    private int getIndexOfCalList(String search_key) {
        for (int temp = 0; temp < calList.size(); temp++) {
            String key = calList.get(temp).get("key");
            if (key != null && key.equals(search_key)) {
                return temp;
            }
        }
        return -1;
    }


GridCellAdapter 에서 변경해줄 것은 아래와 같다.

    private final ArrayList<Calendar_Item> calList;


    public GridCellAdapter(Context context, ArrayList<Calendar_Item> list) {
        mContext = context;
        calList = list;
    }

    public Calendar_Item getItem(int position) {
        return calList.get(position);
    }


        String theday = calList.get(position).getDay();
        int themonth = Integer.parseInt(calList.get(position).getMonth());
        String theyear = calList.get(position).getYear();
        String holiday = calList.get(position).getEvent();
 

    private final ArrayList<HashMap<String, String>> calList;


    public GridCellAdapter(Context context, ArrayList<HashMap<String, String>> list) {
        mContext = context;
        calList = list;
    }


    public HashMap<String, String> getItem(int position) {
        return calList.get(position);
    }

        String theday = calList.get(position).get("day");
        int themonth = Integer.parseInt(calList.get(position).get("month"));
        String theyear = calList.get(position).get("year");
        String holiday = calList.get(position).get("event");
 


ArrayList<HashMap<String, String>> 를 사용하면 아래와 같은 Calendar_Item Class 를 만들 필요가 없어 코드가 좀 더 간단해질 수 있다.

public class Calendar_Item {
    private  String year; // 년
    private  String month; // 월
    private  String day; // 일
    private  int weekday; // 요일
    private  String color; // 색상
    private String event;
    private String key; // 월일 값을 key 로 사용

    public Calendar_Item() {
    }

    public Calendar_Item(String year, String month, String day, int weekday, String color) {
        this.year = year;
        this.month = month;
        this.day = day;
        this.weekday = weekday;
        this.color = color;
    }

    public String getYear() {
        return year;
    }

    public void setYear(String year) {
        this.year = year;
    }

    public String getMonth() {
        return month;
    }

    public void setMonth(String month) {
        this.month = month;
    }

    public String getDay() {
        return day;
    }

    public void setDay(String day) {
        this.day = day;
    }

    public int getWeekday() {
        return weekday;
    }

    public void setWeekday(int weekday) {
        this.weekday = weekday;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public String getEvent() {
        return event;
    }

    public void setEvent(String event) {
        this.event = event;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }
}


블로그 이미지

Link2Me

,
728x90

새로운 Moudle 를 추가했더니 이런 에러가 발생한다.

Manifest merger failed with multiple errors, see logs


보통 다른 블로그에 나온 경우와는 다른 황당한 경우다.


public class Calendar_Item {
    private  String year; // 년
    private  String month; // 월
    private  String day; // 일
    private  int weekday; // 요일
    private  String color; // 색상
    private String event;
    private String key; // 월일 값을 key 로 사용


처음에 이름을 name 으로 했다가 rename 으로 일괄 event 로 변경했더니......

프로젝트내의 모든 AndroidManifest.xml 파일과

res/values/ 하단 파일들이 모두 name → event 로 변경되어 있더라. ㅠㅠ


이런 황당한 경우가 있다니....

그러다보니 AndroidManifest.xml 이 비정상적이다보니 이런 에러메시지를 보여줬던 거다.


모두 수작업으로 EditPlus 에서 event= → name= 로 변경해주고 나서야 정상이 되었다.


변수를 사용할 때 절대로 name 과 같은 걸 사용해서는 안되겠다는 걸 알게 되었다.



블로그 이미지

Link2Me

,
728x90

Calendar Class 를 사용하여 달력을 만드는데 알아둘 사항을 정리했다.

Date는 JDK1.0, Calendar는 JDK1.1 부터 제공되었다.

Calendar는 추상클래스라서 객체를 직접 생성할 수 없고, 메소드를 통해서 완전히 구현된 클래스의 인스턴스를 얻어야 한다.

Calendar cal = new Calendar(); // 에러. 추상클래스는 인스턴스를 생성할 수 없다.

Calendar cal = Calendar.getInstance(); // getInstance()는 Calendar 클래스의 인스턴스를 반환


아래 코드는 Java에서 작성한 코드다.

package Calendar;

import java.util.Calendar;

public class Calendar_EX {

    public static void main(String[] args) {
        // Calendar Class 를 사용한 달력 구현을 위한 기본사항 알아보기
        int year = 2019;
        int month = 2;
        int trailingSpaces = 0;
        int daysInPrevMonth = 0;
        int prevYear = 0;
        int prevMonth = 0;
        int DAY_OFFSET = 1;
       
        Calendar cal = Calendar.getInstance(); //Calendar 객체 생성(시스템의 현재 날짜와 시간 정보)
        System.out.println("today is "+cal.getTime()); // 현재시각을 알려주는 메소드
        System.out.println("올해 년도 : " + cal.get(Calendar.YEAR));
        System.out.println("이번 달(0~11, 0:1월): " + cal.get(Calendar.MONTH));
        System.out.println("이번 달의 오늘 날짜 : " + cal.get(Calendar.DATE));
       
        int LastDays = cal.getActualMaximum(Calendar.DATE); //해당 월 마지막 날짜
        System.out.println("이번달의 마지막날짜 : "+LastDays); // 현재시각을 알려주는 메소드
        int dayofweek = cal.get(Calendar.DAY_OF_WEEK); // 현재 요일 (일요일은 1, 토요일은 7)
        System.out.println("이번달 오늘날짜 요일 값 : "+dayofweek); // 오늘날짜의 요일값을 반환
       
        System.out.println("이번년도의 몇째 주 : "+ cal.get(Calendar.WEEK_OF_YEAR));
        System.out.println("이번달의 몇째 주 : "+ cal.get(Calendar.WEEK_OF_MONTH));
        System.out.println("이번달의 몇째 주 : " + cal.get(Calendar.DAY_OF_WEEK_IN_MONTH));
        System.out.println("현재 시간 : "+ cal.get(Calendar.HOUR)); // 현재 시간 (12시간제)
        System.out.println("현재 시간 : "+ cal.get(Calendar.HOUR_OF_DAY)); // 현재 시간 (24시간제)
        System.out.println("오전_오후(0:오전, 1:오후): " + cal.get(Calendar.AM_PM));
        System.out.println("현재 분 : "+ cal.get(Calendar.MINUTE));
        System.out.println("현재 초 : "+ cal.get(Calendar.SECOND));
        String time = String.valueOf(cal.get(Calendar.HOUR)) + "시 " +
                String.valueOf(cal.get(Calendar.MINUTE)) + "분 " +
                String.valueOf(cal.get(Calendar.SECOND)) + "초" ;
        System.out.println("현재 시간 : "+ time);
       
       
        //set메소드를 통해 연도 월 일 을 설정
        cal.set(Calendar.YEAR, Integer.valueOf(year));
        cal.set(Calendar.MONTH, Integer.valueOf(month)-1); // 0 ~ 11월이라  1을 빼줌
        cal.set(Calendar.DAY_OF_MONTH, 1); // 지정 월의 날짜
        // 년 월 일을 나누어서 표현했고, 아래는 하나로 년월일을 지정
        //cal.set(year,month-1,1); // 0 ~ 11월로 인식하므로 - 1, 입력된 년월

        LastDays = cal.getActualMaximum(Calendar.DATE); //지정한 월의 마지막 날짜
        System.out.println("SET 지정달 마지막날짜 : "+LastDays);

        dayofweek = cal.get(Calendar.DAY_OF_WEEK); // 지정날짜 요일 (일요일은 1, 토요일은 7)
        System.out.println("SET 지정달 week 값 : "+dayofweek); // 지정날짜의 1일의 요일 값을 표시
        trailingSpaces = dayofweek -1;
        prevMonth = month -1;
        prevYear = year;
        daysInPrevMonth = getLastDay(prevMonth,prevYear);
        System.out.println("SET 이전달 마지막날짜 : "+daysInPrevMonth);
        // 이번달 1일 이전 공백 부분을 채우기 위한 이전달 날짜
        for (int i = 0; i < trailingSpaces; i++) {
             System.out.println("SET 이전달 날짜 : "+ String.valueOf((daysInPrevMonth - trailingSpaces + DAY_OFFSET) + i));
        }
    }

    // 달의 마지막 일을 구함
    private static int getLastDay(int month, int year) {
        if (month == 4 || month == 6 || month == 9 || month == 11) {
            return 30;
        } else if (month == 2 && isLeapYear(year) == true) {
            return 29;
        } else if (month == 2 && isLeapYear(year) == false) {
            return 28;
        } else {
            return 31;
        }
    }

    // 해당 년도가 윤년인지 판별
    private static boolean isLeapYear(int year) {
        return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0));
    }
}


블로그 이미지

Link2Me

,
728x90

안드로이드 달력 화면에 뿌려줄 GridCellAdapter 코드는 아래와 같다.


package com.link2me.android.adpater;

import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.link2me.android.calendar.R;
import com.link2me.android.item.Calendar_Item;
import com.link2me.android.util.CalendarHelper;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;

public class GridCellAdapter extends BaseAdapter implements View.OnClickListener {
    private static final String tag = GridCellAdapter.class.getSimpleName();
    private final Context mContext;

    private final ArrayList<Calendar_Item> calList;
    private LinearLayout gridcell_layout;
    private TextView gridcell_Day;
    private TextView gridcell_LunarDay;
    private TextView gridcell_Event;
    private final SimpleDateFormat dateFormatter = new SimpleDateFormat("dd-MMM-yyyy");

    public GridCellAdapter(Context context, ArrayList<Calendar_Item> list) {
        mContext = context;
        calList = list;
    }

    public Calendar_Item getItem(int position) {
        return calList.get(position);
    }

    @Override
    public int getCount() {
        return calList.size();
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = convertView;
        if (view == null) {
            LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            view = inflater.inflate(R.layout.calendar_cell, parent, false);
        }

        // Get a reference to the Day gridcell_layout
        gridcell_layout = (LinearLayout) view.findViewById(R.id.calendar_day_gridcell);
        gridcell_Day = view.findViewById(R.id.dayTV);
        gridcell_LunarDay = view.findViewById(R.id.lunardayTV);
        gridcell_Event = view.findViewById(R.id.eventTV);
        gridcell_layout.setOnClickListener(this);

        String theday = calList.get(position).getDay();
        int themonth = Integer.parseInt(calList.get(position).getMonth());
        String theyear = calList.get(position).getYear();
        String holiday = calList.get(position).getEvent();

        // Set the Day GridCell
        gridcell_Day.setText(theday);
        gridcell_LunarDay.setText(CalendarHelper.Sol2Lun(theyear, String.valueOf(themonth-1),theday)); //0 ~ 11월로 인식하므로 - 1
        if(holiday.length()>0){
            gridcell_Event.setText(holiday);
        } else {
            gridcell_Event.setText("");
        }
        gridcell_layout.setTag(theday + "-" + themonth + "-" + theyear);

        if(calList.get(position).getColor().equals("GRAY")){
            gridcell_Day.setTextColor(Color.LTGRAY);
        }
        if(calList.get(position).getColor().equals("BLACK")){
            gridcell_Day.setTextColor(Color.BLACK);
        }
        if(calList.get(position).getColor().equals("RED")){
            gridcell_Day.setTextColor(Color.RED);
            gridcell_Event.setTextColor(Color.RED);
        }
        if(calList.get(position).getColor().equals("BLUE")){
            gridcell_Day.setTextColor(Color.BLUE);
        }
        if(calList.get(position).getColor().equals("CYAN")){
            gridcell_layout.setBackgroundColor(Color.CYAN);
        }
        return view;
    }

    @Override
    public void onClick(View view) {
        String date_month_year = (String) view.getTag();
        Log.e("Selected date", date_month_year);
        try {
            Date parsedDate = dateFormatter.parse(date_month_year);
            Log.d(tag, "Parsed Date: " + parsedDate.toString());

        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

}


calendar_cell.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/calendar_day_gridcell"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/day_cell_bg"
    android:orientation="vertical"
    android:padding="7dp">

    <TextView
        android:id="@+id/dayTV"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="8"
        android:textStyle="bold"
        android:textSize="24sp" />

    <TextView
        android:id="@+id/lunardayTV"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="00.16"
        android:textSize="10sp" />

    <TextView
        android:id="@+id/eventTV"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:maxLines="1"
        android:text="기념일"
        android:textColor="#ff0000" />

</LinearLayout>



블로그 이미지

Link2Me

,
728x90

CalendarHelper Class 를 생성하여 양력/음력 변환, 음력/양력 변환, 한달의 마지막날짜 구하기 등을 하는 메소드를 구현한다.


package com.link2me.android.util;

import com.ibm.icu.util.ChineseCalendar;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;

public class CalendarHelper {
    /**
     * 음력날짜를 양력날짜로 변환
     * @param 음력날짜 (yyyyMMdd)
     * @return 양력날짜 (yyyyMMdd)
     */
    public static String Lunar2Solar(String yyyymmdd) {
        ChineseCalendar cc = new ChineseCalendar();
        Calendar cal = Calendar.getInstance();

        if (yyyymmdd == null)
            return "";

        String date = yyyymmdd.trim();
        if (date.length() != 8) {
            if (date.length() == 4)
                date = date + "0101";
            else if (date.length() == 6)
                date = date + "01";
            else if (date.length() > 8)
                date = date.substring(0, 8);
            else
                return "";
        }

        cc.set(ChineseCalendar.EXTENDED_YEAR, Integer.parseInt(date.substring(0, 4)) + 2637);
        cc.set(ChineseCalendar.MONTH, Integer.parseInt(date.substring(4, 6)) - 1);
        cc.set(ChineseCalendar.DAY_OF_MONTH, Integer.parseInt(date.substring(6)));

        cal.setTimeInMillis(cc.getTimeInMillis());

        int y = cal.get(Calendar.YEAR);
        int m = cal.get(Calendar.MONTH) + 1;
        int d = cal.get(Calendar.DAY_OF_MONTH);

        StringBuffer ret = new StringBuffer();
        ret.append(String.format("%04d", y));
        ret.append(String.format("%02d", m));
        ret.append(String.format("%02d", d));

        return ret.toString();
    }

    /**
     * 양력날짜를 음력날짜로 변환
     * @param 양력날짜 (yyyyMMdd)
     * @return 음력날짜 (yyyyMMdd)
     */
    public static String Solar2Lunar(String yyyymmdd) {
        ChineseCalendar cc = new ChineseCalendar();
        Calendar cal = Calendar.getInstance();

        if (yyyymmdd == null)
            return "";

        String date = yyyymmdd.trim() ;
        if( date.length() != 8 ) {
            if( date.length() == 4 )
                date = date + "0101" ;
            else if( date.length() == 6 )
                date = date + "01" ;
            else if( date.length() > 8 )
                date = date.substring(0,8) ;
            else
                return "" ;
        }

        cal.set( Calendar.YEAR, Integer.parseInt(date.substring(0,4)) ) ;
        cal.set( Calendar.MONTH, Integer.parseInt(date.substring(4,6))-1 ) ;
        cal.set( Calendar.DAY_OF_MONTH, Integer.parseInt(date.substring(6)) ) ;

        cc.setTimeInMillis( cal.getTimeInMillis() ) ;

        // ChinessCalendar.YEAR 는 1~60 까지의 값만 가지고 ,
        // ChinessCalendar.EXTENDED_YEAR 는 Calendar.YEAR 값과 2637 만큼의 차이를 가진다.
        int y = cc.get(ChineseCalendar.EXTENDED_YEAR)-2637 ;
        int m = cc.get(ChineseCalendar.MONTH)+1 ;
        int d = cc.get(ChineseCalendar.DAY_OF_MONTH) ;

        StringBuffer ret = new StringBuffer() ;
        if( y < 1000 )          ret.append( "0" ) ;
        else if( y < 100 )      ret.append( "00" ) ;
        else if( y < 10 )       ret.append( "000" ) ;
        ret.append( y ) ;

        if( m < 10 ) ret.append( "0" ) ;
        ret.append( m ) ;

        if( d < 10 ) ret.append( "0" ) ;
        ret.append( d ) ;

        return ret.toString() ;
    }

    public static int WeekendValue(String date) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
        Calendar cal = Calendar.getInstance();
        cal.setTime(sdf.parse(date));
        return cal.get(Calendar.DAY_OF_WEEK);
        // Calendar.SUNDAY : 1
        // Calendar.SATURDAY : 7
    }

    /**
     * 양력날짜의 요일을 리턴
     * @param 양력날짜 (yyyyMMdd)
     * @return 요일(int)
     */
    public static int getDayOfWeek(String day) {
        int y = Integer.parseInt(day.substring(0, 4));
        int m = Integer.parseInt(day.substring(4, 6)) - 1;
        int d = Integer.parseInt(day.substring(6));
        Calendar c = Calendar.getInstance();
        c.set(y, m, d);
        return c.get(Calendar.DAY_OF_WEEK);
    }

    public static String Sol2Lun(String year, String month, String day) {
        ChineseCalendar cc = new ChineseCalendar();
        Calendar cal = Calendar.getInstance();

        if (year == null || month == null || day == null)
            return "";

        cal.set( Calendar.YEAR, Integer.parseInt(year) ) ;
        cal.set( Calendar.MONTH, Integer.parseInt(month)) ;
        cal.set( Calendar.DAY_OF_MONTH, Integer.parseInt(day)) ;

        cc.setTimeInMillis( cal.getTimeInMillis() ) ;

        // ChinessCalendar.YEAR 는 1~60 까지의 값만 가지고 ,
        // ChinessCalendar.EXTENDED_YEAR 는 Calendar.YEAR 값과 2637 만큼의 차이를 가진다.
        int y = cc.get(ChineseCalendar.EXTENDED_YEAR)-2637 ;
        int m = cc.get(ChineseCalendar.MONTH)+1 ;
        int d = cc.get(ChineseCalendar.DAY_OF_MONTH) ;

        StringBuffer ret = new StringBuffer() ;
        if( y < 1000 )          ret.append( "0" ) ;
        else if( y < 100 )      ret.append( "00" ) ;
        else if( y < 10 )       ret.append( "000" ) ;
        ret.append( y ) ;

        if( m < 10 ) ret.append( "0" ) ;
        ret.append( m ) ;

        if( d < 10 ) ret.append( "0" ) ;
        ret.append( d ) ;

        return ret.toString().substring(4,6)+"."+ret.toString().substring(6,8);
    }

    // 해당 달의 첫 요일을 구해서 돌려줌.
    private static int getStartDay(int year, int month) {
        int monthSum = 0;
        int leapYear = 0;
        int daySum = 1;

        for (int i = 1; i < year; i++) {
            monthSum += 365;
            if (isLeapYear(i) == true) {
                leapYear += 1;
            }
        }

        for (int j = 1; j < month; j++) {
            daySum += getLastDay(year, j);
        }

        return (monthSum + leapYear + daySum) % 7;

    }

    // 달의 마지막 일을 구함
    public static int getLastDay(int month, int year) {

        if (month == 4 || month == 6 || month == 9 || month == 11) {
            return 30;
        } else if (month == 2 && isLeapYear(year) == true) {
            return 29;
        } else if (month == 2 && isLeapYear(year) == false) {
            return 28;
        } else {
            return 31;
        }
    }

    // 해당 년도가 윤년인지 판별
    private static boolean isLeapYear(int year) {
        return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0));
    }
}


CalendarHelper 수정본

package com.link2me.android.util;

import com.ibm.icu.util.ChineseCalendar;
import com.link2me.android.item.Holidays_Item;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;


public class CalendarHelper {
    static HashMap<String, Holidays_Item> holidaysArrayList = new HashMap<String,Holidays_Item>();

    /**
     * 음력날짜를 양력날짜로 변환
     * @param 음력날짜 (yyyyMMdd)
     * @return 양력날짜 (yyyyMMdd)
     */
    public static String Lunar2Solar(String yyyymmdd) {
        ChineseCalendar cc = new ChineseCalendar();
        Calendar cal = Calendar.getInstance();

        if (yyyymmdd == null)
            return "";

        String date = yyyymmdd.trim();
        if (date.length() != 8) {
            if (date.length() == 4)
                date = date + "0101";
            else if (date.length() == 6)
                date = date + "01";
            else if (date.length() > 8)
                date = date.substring(0, 8);
            else
                return "";
        }

        cc.set(ChineseCalendar.EXTENDED_YEAR, Integer.parseInt(date.substring(0, 4)) + 2637);
        cc.set(ChineseCalendar.MONTH, Integer.parseInt(date.substring(4, 6)) - 1);
        cc.set(ChineseCalendar.DAY_OF_MONTH, Integer.parseInt(date.substring(6)));

        cal.setTimeInMillis(cc.getTimeInMillis());

        int y = cal.get(Calendar.YEAR);
        int m = cal.get(Calendar.MONTH) + 1;
        int d = cal.get(Calendar.DAY_OF_MONTH);

        StringBuffer ret = new StringBuffer();
        ret.append(String.format("%04d", y));
        ret.append(String.format("%02d", m));
        ret.append(String.format("%02d", d));

        return ret.toString();
    }

    /**
     * 양력날짜를 음력날짜로 변환
     * @param 양력날짜 (yyyyMMdd)
     * @return 음력날짜 (yyyyMMdd)
     */
    public static String Solar2Lunar(String yyyymmdd) {
        ChineseCalendar cc = new ChineseCalendar();
        Calendar cal = Calendar.getInstance();

        if (yyyymmdd == null)
            return "";

        String date = yyyymmdd.trim() ;
        if( date.length() != 8 ) {
            if( date.length() == 4 )
                date = date + "0101" ;
            else if( date.length() == 6 )
                date = date + "01" ;
            else if( date.length() > 8 )
                date = date.substring(0,8) ;
            else
                return "" ;
        }

        cal.set( Calendar.YEAR, Integer.parseInt(date.substring(0,4)) ) ;
        cal.set( Calendar.MONTH, Integer.parseInt(date.substring(4,6))-1 ) ;
        cal.set( Calendar.DAY_OF_MONTH, Integer.parseInt(date.substring(6)) ) ;

        cc.setTimeInMillis( cal.getTimeInMillis() ) ;

        // ChinessCalendar.YEAR 는 1~60 까지의 값만 가지고 ,
        // ChinessCalendar.EXTENDED_YEAR 는 Calendar.YEAR 값과 2637 만큼의 차이를 가진다.
        int y = cc.get(ChineseCalendar.EXTENDED_YEAR)-2637 ;
        int m = cc.get(ChineseCalendar.MONTH)+1 ;
        int d = cc.get(ChineseCalendar.DAY_OF_MONTH) ;

        StringBuffer ret = new StringBuffer() ;
        if( y < 1000 )          ret.append( "0" ) ;
        else if( y < 100 )      ret.append( "00" ) ;
        else if( y < 10 )       ret.append( "000" ) ;
        ret.append( y ) ;

        if( m < 10 ) ret.append( "0" ) ;
        ret.append( m ) ;

        if( d < 10 ) ret.append( "0" ) ;
        ret.append( d ) ;

        return ret.toString() ;
    }


    public static HashMap<String,Holidays_Item> holidayArray(String yyyy){
        holidaysArrayList.clear(); // 데이터 초기화
        // 양력 휴일
        addHolidaysItem(yyyy,"0101" ,"신정");
        addHolidaysItem(yyyy,"0301" ,"삼일절");
        addHolidaysItem(yyyy,"0505" ,"어린이날");
        addHolidaysItem(yyyy,"0606" ,"현충일" );
        addHolidaysItem(yyyy,"0815" ,"광복절");
        addHolidaysItem(yyyy,"1003" ,"개천절");
        addHolidaysItem(yyyy,"1009" ,"한글날");
        addHolidaysItem(yyyy,"1225" ,"성탄절");

        // 음력 휴일
        String prev_seol = String.valueOf(Integer.parseInt(Lunar2Solar(yyyy+"0101")) -1);
        addHolidaysItem(yyyy,prev_seol.substring(4) ,"");
        addHolidaysItem(yyyy,SolarDays(yyyy, "0101"),"설날");
        addHolidaysItem(yyyy,SolarDays(yyyy, "0102"),"");
        addHolidaysItem(yyyy,SolarDays(yyyy, "0408"),"석탄일");
        addHolidaysItem(yyyy,SolarDays(yyyy, "0814"),"");
        addHolidaysItem(yyyy,SolarDays(yyyy, "0815"),"추석");
        addHolidaysItem(yyyy,SolarDays(yyyy, "0816"),"");

        try {
            // 어린이날 대체공휴일 검사 : 어린이날은 토요일, 일요일인 경우 그 다음 평일을 대체공유일로 지정
            int childDayChk = WeekendValue(yyyy+"0505");
            if(childDayChk == 1) addHolidaysItem(yyyy,"0506" ,"대체공휴일");
            if(childDayChk == 7) addHolidaysItem(yyyy,"0507" ,"대체공휴일");

            // 설날 대체공휴일 검사
            if(WeekendValue(Lunar2Solar(yyyy+"0101"))==1) addHolidaysItem(yyyy,SolarDays(yyyy, "0103"),"대체공휴일");
            if(WeekendValue(Lunar2Solar(yyyy+"0101"))==2) addHolidaysItem(yyyy,SolarDays(yyyy, "0103"),"대체공휴일");
            if(WeekendValue(Lunar2Solar(yyyy+"0102"))==1) addHolidaysItem(yyyy,SolarDays(yyyy, "0103"),"대체공휴일");

            // 추석 대체공휴일 검사
            if(WeekendValue(Lunar2Solar(yyyy+"0814"))==1) addHolidaysItem(yyyy,SolarDays(yyyy, "0817"),"대체공휴일");
            if(WeekendValue(Lunar2Solar(yyyy+"0815"))==1) addHolidaysItem(yyyy,SolarDays(yyyy, "0817"),"대체공휴일");
            if(WeekendValue(Lunar2Solar(yyyy+"0816"))==1) addHolidaysItem(yyyy,SolarDays(yyyy, "0817"),"대체공휴일");
        } catch (ParseException e) {
            e.printStackTrace();
        }

        //Collections.sort(holidaysArrayList); // 오름차순 정렬

        return holidaysArrayList;
    }

    private static String SolarDays(String yyyy, String date){
        return Lunar2Solar(yyyy+date).substring(4);
    }

    private static void addHolidaysItem(String year, String date, String name ){
        Holidays_Item item = new Holidays_Item();
        item.setYear(year);
        item.setDate(date);
        item.setName(name);
        holidaysArrayList.put(date,item);
    }

    public static int WeekendValue(String date) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
        Calendar cal = Calendar.getInstance();
        cal.setTime(sdf.parse(date));
        return cal.get(Calendar.DAY_OF_WEEK);
        // Calendar.SUNDAY : 1
        // Calendar.SATURDAY : 7
    }

    /**
     * 양력날짜의 요일을 리턴
     * @param 양력날짜 (yyyyMMdd)
     * @return 요일(int)
     */
    public static int getDayOfWeek(String day) {
        int y = Integer.parseInt(day.substring(0, 4));
        int m = Integer.parseInt(day.substring(4, 6)) - 1;
        int d = Integer.parseInt(day.substring(6));
        Calendar c = Calendar.getInstance();
        c.set(y, m, d);
        return c.get(Calendar.DAY_OF_WEEK);
    }

    public static String Sol2Lun(String year, String month, String day) {
        ChineseCalendar cc = new ChineseCalendar();
        Calendar cal = Calendar.getInstance();

        if (year == null || month == null || day == null)
            return "";

        cal.set( Calendar.YEAR, Integer.parseInt(year) ) ;
        cal.set( Calendar.MONTH, Integer.parseInt(month)) ;
        cal.set( Calendar.DAY_OF_MONTH, Integer.parseInt(day)) ;

        cc.setTimeInMillis( cal.getTimeInMillis() ) ;

        // ChinessCalendar.YEAR 는 1~60 까지의 값만 가지고 ,
        // ChinessCalendar.EXTENDED_YEAR 는 Calendar.YEAR 값과 2637 만큼의 차이를 가진다.
        int y = cc.get(ChineseCalendar.EXTENDED_YEAR)-2637 ;
        int m = cc.get(ChineseCalendar.MONTH)+1 ;
        int d = cc.get(ChineseCalendar.DAY_OF_MONTH) ;

        StringBuffer ret = new StringBuffer() ;
        if( y < 1000 )          ret.append( "0" ) ;
        else if( y < 100 )      ret.append( "00" ) ;
        else if( y < 10 )       ret.append( "000" ) ;
        ret.append( y ) ;

        if( m < 10 ) ret.append( "0" ) ;
        ret.append( m ) ;

        if( d < 10 ) ret.append( "0" ) ;
        ret.append( d ) ;

        return ret.toString().substring(4,6)+"."+ret.toString().substring(6,8);
    }

    // 해당 달의 첫 요일을 구해서 돌려줌.
    private static int getStartDay(int year, int month) {
        int monthSum = 0;
        int leapYear = 0;
        int daySum = 1;

        for (int i = 1; i < year; i++) {
            monthSum += 365;
            if (isLeapYear(i) == true) {
                leapYear += 1;
            }
        }

        for (int j = 1; j < month; j++) {
            daySum += getLastDay(year, j);
        }

        return (monthSum + leapYear + daySum) % 7;

    }

    // 달의 마지막 일을 구함
    public static int getLastDay(int month, int year) {

        if (month == 4 || month == 6 || month == 9 || month == 11) {
            return 30;
        } else if (month == 2 && isLeapYear(year) == true) {
            return 29;
        } else if (month == 2 && isLeapYear(year) == false) {
            return 28;
        } else {
            return 31;
        }
    }

    // 해당 년도가 윤년인지 판별
    private static boolean isLeapYear(int year) {
        return (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0));
    }



블로그 이미지

Link2Me

,
728x90

달력 데이터를 생성하는 것은 ArrayList 또는 Hashmap 을 이용한다.


먼저 달력 데이터 Class 를 구현한다.

package com.link2me.android.item;

public class Calendar_Item {
    private  String year; // 년
    private  String month; // 월
    private  String day; // 일
    private  int weekday; // 요일
    private  String color; // 색상
    private String event;
    private String key; // 월일 값을 key 로 사용

    public Calendar_Item() {
    }

    public Calendar_Item(String year, String month, String day, int weekday, String color) {
        this.year = year;
        this.month = month;
        this.day = day;
        this.weekday = weekday;
        this.color = color;
    }

    public String getYear() {
        return year;
    }

    public void setYear(String year) {
        this.year = year;
    }

    public String getMonth() {
        return month;
    }

    public void setMonth(String month) {
        this.month = month;
    }

    public String getDay() {
        return day;
    }

    public void setDay(String day) {
        this.day = day;
    }

    public int getWeekday() {
        return weekday;
    }

    public void setWeekday(int weekday) {
        this.weekday = weekday;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public String getEvent() {
        return event;
    }

    public void setEvent(String event) {
        this.event = event;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }
}


먼저 작성했던 MainActivity 에서 makeCalendarList(int CurrentMonth, int yy) 메소드를 추가하면서 필요한 코드를 작성한다.

여기서는 공휴일 데이터를 추가하는 루틴을 생략했다.

공휴일 데이터 추가 샘플은 https://link2me.tistory.com/1699 게시글과 다음 게시글인 CalendarHelper 를 같이 참조하며 CalendarHelper 에 필요한 사항을 추가하고 수정해서 구현하도록 개발자의 몫으로 남겼다.


class package com.link2me.android.calendar;

import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import com.link2me.android.adpater.GridCellAdapter;
import com.link2me.android.item.Calendar_Item;
import com.link2me.android.item.Holidays_Item;
import com.link2me.android.util.CalendarHelper;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Locale;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = MainActivity.class.getSimpleName();
    private Context mContext;

    private TextView currentMonth;
    private ImageView prevYear;
    private ImageView prevMonth;
    private ImageView nextMonth;
    private ImageView nextYear;
    private ImageView selectDate;

    private GridView calendarView;
    private GridCellAdapter adapter;
    private Calendar cal;
    private int month, year;
    private static final String dateTemplate = "yyyy MMMM";

    private ArrayList<Calendar_Item> calList = new ArrayList<>();
    private static final int DAY_OFFSET = 1;
    private final String[] weekdays = new String[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
    private int lastDays;

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

        cal = Calendar.getInstance(Locale.getDefault());
        month = cal.get(Calendar.MONTH) + 1;
        year = cal.get(Calendar.YEAR);

        prevYear = (ImageView) this.findViewById(R.id.prevYear);
        prevYear.setOnClickListener(this);

        prevMonth = (ImageView) this.findViewById(R.id.prevMonth);
        prevMonth.setOnClickListener(this);

        currentMonth = (TextView) this.findViewById(R.id.currentMonth);
        currentMonth.setText(year +"년 " + month + "월");
        currentMonth.setOnClickListener(this);

        nextMonth = (ImageView) this.findViewById(R.id.nextMonth);
        nextMonth.setOnClickListener(this);

        nextYear = (ImageView) this.findViewById(R.id.nextYear);
        nextYear.setOnClickListener(this);

        selectDate = (ImageView) this.findViewById(R.id.selectDate);
        selectDate.setOnClickListener(this);

        calendarView = (GridView) this.findViewById(R.id.calendar);

        makeCalendarList(month, year);

        // Initialised
        adapter = new GridCellAdapter(mContext, calList);
        adapter.notifyDataSetChanged();
        calendarView.setAdapter(adapter);
    }

    private void setGridCellAdapterToDate(int month, int year) {
        makeCalendarList(month, year);
        adapter = new GridCellAdapter(mContext, calList);
        adapter.notifyDataSetChanged();
        calendarView.setAdapter(adapter);
        // 상단 화면 날짜 출력
        currentMonth.setText(year +"년 " + month + "월");
    }

    private void makeCalendarList(int CurrentMonth, int yy) {
        int trailingSpaces = 0;
        int daysInPrevMonth = 0;
        int prevMonth = 0;
        int prevYear = 0;
        int nextMonth = 0;
        int nextYear = 0;

        calList.clear();

        Calendar cal = Calendar.getInstance();

        int thisYear = cal.get(Calendar.YEAR);

        int thisMonth = cal.get(Calendar.MONTH); // 현재 월
        int thisDay = cal.get(Calendar.DAY_OF_MONTH); // 오늘 날짜
        cal.set(yy,CurrentMonth-1,1); // 0 ~ 11월로 인식하므로 - 1, 입력된 년월
        Log.e(TAG, "Calendar SET := " + cal.getTime().toString());
        lastDays = cal.getActualMaximum(Calendar.DAY_OF_MONTH);

        if (CurrentMonth == 12) {
            prevMonth = CurrentMonth - 1;
            nextMonth = 1;
            prevYear = yy;
            nextYear = yy + 1;
            daysInPrevMonth = CalendarHelper.getLastDay(prevMonth,prevYear);
        } else if (CurrentMonth == 1) {
            prevMonth = 12;
            prevYear = yy - 1;
            nextYear = yy;
            nextMonth = CurrentMonth + 1;
            daysInPrevMonth = CalendarHelper.getLastDay(prevMonth,prevYear);
        } else {
            prevMonth = CurrentMonth - 1;
            nextMonth = CurrentMonth + 1;
            nextYear = yy;
            prevYear = yy;
            daysInPrevMonth = CalendarHelper.getLastDay(prevMonth,prevYear);
        }

        int currentWeekDay = cal.get(Calendar.DAY_OF_WEEK) - 1;
        trailingSpaces = currentWeekDay;

        // 이전월 데이터 채우기 위한 리스트 추가
        for (int i = 0; i < trailingSpaces; i++) {
            String key = String.format("%02d",prevMonth)+String.format("%02d",(daysInPrevMonth - trailingSpaces + DAY_OFFSET) + i);
            addCalendarItem(String.valueOf(prevYear),String.valueOf(prevMonth),String.valueOf((daysInPrevMonth - trailingSpaces + DAY_OFFSET) + i),
                    0,"GRAY","",key);
        }

        // 현재월 데이터 리스트 추가
        for (int i = 1; i <= lastDays; i++) {
            String date = String.valueOf(yy)+String.format("%02d",CurrentMonth)+String.format("%02d",i);
            String key = String.format("%02d",CurrentMonth)+String.format("%02d",i);
            try {
                int weekday = CalendarHelper.WeekendValue(date);
                if(weekday == 1) // 일요일
                    addCalendarItem(String.valueOf(yy),String.valueOf(CurrentMonth),String.valueOf(i),weekday,"RED","",key);
                else if(weekday == 7) // 토요일
                    addCalendarItem(String.valueOf(yy),String.valueOf(CurrentMonth),String.valueOf(i),weekday,"BLUE","",key);
                else
                    addCalendarItem(String.valueOf(yy),String.valueOf(CurrentMonth),String.valueOf(i),weekday,"BLACK","",key);
                if(CurrentMonth-1 == thisMonth){ // 현재월이면
                    if(i == thisDay){
                        Log.e(TAG, "key := " + key);
                        int index = getIndexOfCalList(key);
                        Calendar_Item item = calendarItem(String.valueOf(yy),String.valueOf(CurrentMonth),String.valueOf(i),weekday,"CYAN","",key);
                        calList.set(index,item);
                    }
                }
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }

        // 다음달 데이터 채우기 리스트 추가
        for (int i = 0; i < calList.size() % 7; i++) {
            String key = String.format("%02d",nextMonth)+String.format("%02d",i+1);
            addCalendarItem(String.valueOf(nextYear),String.valueOf(nextMonth),String.valueOf(i+1),0,"GRAY",
                    "",key);
        }

    }

    private Calendar_Item calendarItem(String year, String month, String day, int weekday, String color, String name, String key){
        Calendar_Item item = new Calendar_Item();
        item.setYear(year);
        item.setMonth(month);
        item.setDay(day);
        item.setWeekday(weekday);
        item.setColor(color);
        item.setEvent(name);
        item.setKey(key);
        return item;
    }

    private void addCalendarItem(String year, String month, String day, int weekday, String color,String name, String key){
        Calendar_Item item = calendarItem(year,month,day,weekday,color,name,key);
        calList.add(item);
    }

    private int getIndexOfCalList(String search_key) {
        for (int temp = 0; temp < calList.size(); temp++) {
            String key = calList.get(temp).getKey();
            if (key != null && key.equals(search_key)) {
                return temp;
            }
        }
        return -1;
    }

    private String getWeekDayAsString(int i) {
        return weekdays[i];
    }
}


참고 도움될 자료 : https://www.toptal.com/android/android-customization-how-to-build-a-ui-component-that-does-what-you-want


다음은 CalendarHelper Class 를 만들어서 음력/양력 변환, 공휴일 처리, 한달의 마지막 날짜 구하기 등 메소드를 구현한다.

블로그 이미지

Link2Me

,
728x90

Layout 을 화면에 뿌릴 MainActivity.java 코드를 아래와 같이 구현했다.


package com.link2me.android.calendar;

import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import com.link2me.android.adpater.GridCellAdapter;

import java.util.Calendar;
import java.util.Locale;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = MainActivity.class.getSimpleName();
    private Context mContext;

    private TextView currentMonth;
    private ImageView prevYear;
    private ImageView prevMonth;
    private ImageView nextMonth;
    private ImageView nextYear;
    private ImageView selectDate;

    private GridView calendarView;
    private GridCellAdapter adapter;
    private Calendar cal;
    private int month, year;

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

        cal = Calendar.getInstance(Locale.getDefault());
        month = cal.get(Calendar.MONTH) + 1;
        year = cal.get(Calendar.YEAR);

        prevYear = (ImageView) this.findViewById(R.id.prevYear);
        prevYear.setOnClickListener(this);

        prevMonth = (ImageView) this.findViewById(R.id.prevMonth);
        prevMonth.setOnClickListener(this);

        currentMonth = (TextView) this.findViewById(R.id.currentMonth);
        currentMonth.setText(year +"년 " + month + "월");
        currentMonth.setOnClickListener(this);

        nextMonth = (ImageView) this.findViewById(R.id.nextMonth);
        nextMonth.setOnClickListener(this);

        nextYear = (ImageView) this.findViewById(R.id.nextYear);
        nextYear.setOnClickListener(this);

        selectDate = (ImageView) this.findViewById(R.id.selectDate);
        selectDate.setOnClickListener(this);

        calendarView = (GridView) this.findViewById(R.id.calendar);

        // Initialised
        adapter = new GridCellAdapter(mContext, month, year);
        adapter.notifyDataSetChanged();
        calendarView.setAdapter(adapter);
    }

    private void setGridCellAdapterToDate(int month, int year) {
        adapter = new GridCellAdapter(mContext, month, year);
        adapter.notifyDataSetChanged();
        calendarView.setAdapter(adapter);
        // 상단 화면 날짜 출력
        currentMonth.setText(year +"년 " + month + "월");
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.prevYear:
                year--;
                setGridCellAdapterToDate(month, year);
                break;

            case R.id.prevMonth:
                if (month <= 1) {
                    month = 12;
                    year--;
                } else {
                    month--;
                }
                setGridCellAdapterToDate(month, year);
                break;

            case R.id.currentMonth:
                cal = Calendar.getInstance(Locale.getDefault()); // 현재날짜와 시간으로 설정된다.
                month = cal.get(Calendar.MONTH) + 1; // 0 ~ 11월을 반환하므로 + 1을 해준다.
                year = cal.get(Calendar.YEAR);
                setGridCellAdapterToDate(month, year);
                break;

            case R.id.nextMonth:
                if (month > 11) {
                    month = 1;
                    year++;
                } else {
                    month++;
                }
                setGridCellAdapterToDate(month, year);
                break;

            case R.id.nextYear:
                year++;
                setGridCellAdapterToDate(month, year);
                break;

            case R.id.selectDate:
                LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
                final View layout = inflater.inflate(R.layout.select_calendar_input,null);
                AlertDialog.Builder cal_confirm = new AlertDialog.Builder(mContext);
                cal_confirm.setTitle("검색할 년도 입력");
                cal_confirm.setView(layout);
                final EditText etYear = layout.findViewById(R.id.selectYear);
                final EditText etMonth = layout.findViewById(R.id.selectMonth);
                // 확인 버튼 설정
                cal_confirm.setPositiveButton("검색", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        String selectYear = etYear.getText().toString().trim();
                        if(selectYear.length() == 0) {
                            AlertDialog.Builder phoneNO_confirm = new AlertDialog.Builder(mContext);
                            phoneNO_confirm.setMessage("년도를 입력하세요.").setCancelable(false).setPositiveButton("확인",
                                    new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) { // 'YES'
                                            dialog.dismiss();
                                        }
                                    });
                            AlertDialog alert = phoneNO_confirm.create();
                            alert.show();
                            return;
                        }

                        String selectMonth = etMonth.getText().toString().trim();
                        if(selectMonth.length() == 0) {
                            AlertDialog.Builder phoneNO_confirm = new AlertDialog.Builder(mContext);
                            phoneNO_confirm.setMessage("월(月)을 입력하세요.").setCancelable(false).setPositiveButton("확인",
                                    new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) { // 'YES'
                                            dialog.dismiss();
                                        }
                                    });
                            AlertDialog alert = phoneNO_confirm.create();
                            alert.show();
                            return;
                        }
                        month = Integer.parseInt(selectMonth);
                        year = Integer.parseInt(selectYear);
                        setGridCellAdapterToDate(month, year);
                    }
                });
                cal_confirm.setNegativeButton("취소", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                });
                cal_confirm.show();
                break;
        }
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "Destroying View ...");
        super.onDestroy();
    }
}


화면에 출력할 데이터는 GridCellAdapter 를 만들어서 뿌려주면 된다.

블로그 이미지

Link2Me

,
728x90

만들고 싶은 달력의 모습은 아래와 같다.

이미지 파일은 https://www.iconfinder.com/ 에서 free 이미지를 받아서 이미지 크기를 줄여서 사용했다.


 



AndroidManifest.xml

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28


    defaultConfig {
        applicationId "com.link2me.android.calendar"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation files('libs/icu4j-4_8_2.jar')
}


Android 달력 만들기 Layout 구성을 위한 XML 파일 구조다.

전년도, 이전달, 현재 월, 다음달, 다음년도, 특정 년월 검색

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@color/lightgray"
    android:orientation="vertical" >

    <LinearLayout
        android:id="@+id/buttonlayout"
        android:layout_width="fill_parent"
        android:layout_height="60sp"
        android:background="@drawable/navibar_bg"
        android:gravity="left|top"
        android:height="60sp"
        android:orientation="horizontal" >

        <LinearLayout
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center">

            <ImageView
                android:id="@+id/prevYear"
                android:layout_width="wrap_content"
                android:layout_height="35dp"
                android:layout_gravity="center"
                android:src="@drawable/double_arrow_left" >
            </ImageView>
        </LinearLayout>

        <LinearLayout
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center">
            <ImageView
                android:id="@+id/prevMonth"
                android:layout_width="wrap_content"
                android:layout_height="35dp"
                android:layout_gravity="center"
                android:src="@drawable/arrow_left" >
            </ImageView>
        </LinearLayout>

        <TextView
            android:id="@+id/currentMonth"
            android:layout_width="fill_parent"
            android:layout_height="60sp"
            android:layout_weight="0.6"
            android:gravity="center"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textColor="#FFFFFF" >
        </TextView>

        <LinearLayout
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center">

            <ImageView
                android:id="@+id/nextMonth"
                android:layout_width="wrap_content"
                android:layout_height="35dp"
                android:layout_gravity="center"
                android:src="@drawable/arrow_right" >
            </ImageView>

        </LinearLayout>

        <LinearLayout
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center">
            <ImageView
                android:id="@+id/nextYear"
                android:layout_width="wrap_content"
                android:layout_height="35dp"
                android:layout_gravity="center"
                android:src="@drawable/double_arrow_right" >
            </ImageView>
        </LinearLayout>

        <LinearLayout
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center">
            <ImageView
                android:id="@+id/selectDate"
                android:layout_width="wrap_content"
                android:layout_height="35dp"
                android:layout_gravity="center"
                android:src="@drawable/box_arrow" >
            </ImageView>
        </LinearLayout>

    </LinearLayout>

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="30dp"
        android:background="#e0e7ee"
        android:gravity="center"
        android:orientation="horizontal">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="일"
            android:textColor="#ff0000"
            android:textSize="14sp"
            android:textStyle="bold" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="월"
            android:textColor="#969a9b"
            android:textSize="14sp"
            android:textStyle="bold" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="화"
            android:textColor="#969a9b"
            android:textSize="14sp"
            android:textStyle="bold" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="수"
            android:textColor="#969a9b"
            android:textSize="14sp"
            android:textStyle="bold" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="목"
            android:textColor="#969a9b"
            android:textSize="14sp"
            android:textStyle="bold" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="금"
            android:textColor="#969a9b"
            android:textSize="14sp"
            android:textStyle="bold" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:text="토"
            android:textColor="#ff0000"
            android:textSize="14sp"
            android:textStyle="bold" />
    </LinearLayout>

    <GridView
        android:id="@+id/calendar"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:numColumns="7" >
    </GridView>

</LinearLayout>


select_calendar_input.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:text="년도"
            android:layout_marginLeft="20dp"
            android:textSize="12sp" />

        <EditText
            android:id="@+id/selectYear"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_marginRight="20dp"
            android:singleLine="true"
            android:inputType="number"
            android:textSize="16sp"
            android:textColor="#000000" />

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="20dp"
            android:text="월(月)"
            android:textSize="12sp" />

        <EditText
            android:id="@+id/selectMonth"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_marginRight="20dp"
            android:singleLine="true"
            android:inputType="number"
            android:textSize="16sp"
            android:textColor="#000000" />

    </LinearLayout>
</LinearLayout>


블로그 이미지

Link2Me

,
728x90

달력을 구현하다보니 September 와 같은 영문 월에 대한 입력을 받아서 숫자 월을 반환하는 코드가 필요하다.

양력 → 음력으로 날짜를 반환할 목적이라서 숫자 대신에 숫자 문자열로 반환하도록 처리했다.


private static String ConvertMonthString(String month){
    // September 와 같은 입력을 9와 같은 숫자 문자열로 반환
    Date date = null;
    try {
        date = new SimpleDateFormat("MMM", Locale.ENGLISH).parse(month);
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return String.valueOf(cal.get(Calendar.MONTH));
    } catch (ParseException e) {
        e.printStackTrace();
        return "";
    }
}


블로그 이미지

Link2Me

,
728x90

현재 위치를 문자로 보내기

현재 내 위치를 문자로 상대방에게 보내는 기능을 구현 테스트 해봤다.

2010년대 초반대에 지도가 막 대세로 떠오르던 그 시절에 서비스 기획하면서 내 위치를 상대방에게 보내주면 주변에서 내가 헤메고 있을 때 유용할 거 같았다.

개인정보 등에 민감한 사항이지만 내 위치를 보내주는 것은 괜찮지 않을까 하는 생각이 들었다.

어디까지나 기능 구현 차원에서 접근하는 것이니 이점은 감안하고 참고하시길 ^^


AndroidManifest.xml

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- GPS -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.SEND_SMS" />



send_message_popup.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <EditText
        android:id="@+id/phoneno"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:singleLine="true"
        android:textSize="16sp"
        android:textColor="#000000" />

</LinearLayout>


https://link2me.tistory.com/1703에서  setCurrentLocation 메소드 부분을 아래와 같이 수정한다.


public void setCurrentLocation(final Location location, String markerTitle, String markerSnippet) {
    if (currentMarker != null) currentMarker.remove();

    LatLng currentLatLng = new LatLng(location.getLatitude(), location.getLongitude());

    MarkerOptions markerOptions = new MarkerOptions();
    markerOptions.position(currentLatLng);
    markerOptions.title(markerTitle);
    markerOptions.snippet(markerSnippet);
    markerOptions.draggable(true);

    currentMarker = mMap.addMarker(markerOptions);
    mMap.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() {
        @Override
        public boolean onMarkerClick(Marker marker) {
            LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(LAYOUT_INFLATER_SERVICE);
            final View layout = inflater.inflate(R.layout.send_message_popup,null);
            AlertDialog.Builder lms_confirm = new AlertDialog.Builder(mContext);
            lms_confirm.setTitle("발송할 휴대폰번호 등록");
            lms_confirm.setView(layout);
            final EditText etphoneNO = (EditText) layout.findViewById(R.id.phoneno);
            // 확인 버튼 설정
            lms_confirm.setPositiveButton("등록", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    String phoneNO = etphoneNO.getText().toString().trim();
                    if(phoneNO.length() == 0) {
                        AlertDialog.Builder phoneNO_confirm = new AlertDialog.Builder(mContext);
                        phoneNO_confirm.setMessage("휴대폰 번호를 입력하세요.").setCancelable(false).setPositiveButton("확인",
                                new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) { // 'YES'
                                        dialog.dismiss();
                                    }
                                });
                        AlertDialog alert = phoneNO_confirm.create();
                        alert.show();
                        return;
                    }
                    String gps_location =String.valueOf(location.getLatitude())+","+String.valueOf(location.getLongitude());
                    SMS_Send(phoneNO,gps_location );
                }
            });
            lms_confirm.setNegativeButton("취소", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                }
            });
            lms_confirm.show();
            return true;
        }
    });

    CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLng(currentLatLng);
    mMap.moveCamera(cameraUpdate);
}

private void SMS_Send(String phoneNO, String message){ // SmsManager API
    String sms_message = "구글 지도 위치를 보내왔습니다.\n";
    sms_message += "http://maps.google.com/maps?f=q&q="+message+"\n"+"누르면 상대방의 위치를 확인할 수 있습니다.";
    Log.e(TAG,"SMS : "+sms_message);
    try {
        //전송
        SmsManager smsManager = SmsManager.getDefault();
        ArrayList<String> parts = smsManager.divideMessage(sms_message);
        smsManager.sendMultipartTextMessage(phoneNO, null, parts, null, null);
        Toast.makeText(getApplicationContext(), "위치전송 문자보내기 완료!", Toast.LENGTH_LONG).show();
    } catch (Exception e) {
        Toast.makeText(getApplicationContext(), "SMS faild, please try again later!", Toast.LENGTH_LONG).show();
        e.printStackTrace();
    }
}


2019.12.15 일 추가 사항

SMS 보내기가 안된다는 댓글 문의가 있어서 적는다.

위험권한에 해당하는 기능은 별도 처리를 해줘야 한다.

https://www.tutorialspoint.com/android/android_sending_sms.htm 에 나온 내용을 참조하거나, https://link2me.tistory.com/1532 에 설명된 TED 퍼미션 사용법을 이용하면 된다.


도움이 되셨다면 ... 해 주세요. 좋은 글 작성에 큰 힘이 됩니다.

블로그 이미지

Link2Me

,
728x90

구글 제공 구글맵 샘플코드는 https://github.com/googlemaps/android-samples/tree/master 에서 다운로드 받아서 필요한 것을 구현하면 된다.


구글 맵에 현재 위치를 표시하는 걸 Activity 위헤서 동작하는 걸 구현 테스트했다면, 이제 Fragment 상에서 동작되도록 하는 법을 기술한다.


<!-- Activity에서는 fragment로 구현해야 되지만 Fragment로 구현하기위해서는 MapView를 주로 사용-->
<com.google.android.gms.maps.MapView
    android:id="@+id/map"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.google.android.gms.maps.MapFragment" />


Fragment 생애 주기를 고려해서 처리하지 않으면 배터리 소모나 메모리 누수가 발생할 수 있으니 이런 것까지 고려해서 처리한다.

소스코드를 비교해서 보면 알겠지만 이전 게시글(https://link2me.tistory.com/1703) 코드를 거의 붙여넣기 했는데 들어갈 위치가 좀 다른 것이 있다는 점만 고려하면 된다.



activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/tabmenuLayout" />

    <LinearLayout
        android:id="@+id/tabmenuLayout"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:orientation="horizontal"
        android:layout_alignParentBottom="true" >

        <Button
            android:id="@+id/btn_tab1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="구글맵"/>

        <Button
            android:id="@+id/btn_tab2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="내 위치"/>

        <Button
            android:id="@+id/btn_tab3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Tab 3"/>


    </LinearLayout>

</RelativeLayout>


fragment_02.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000">

    <com.google.android.gms.maps.MapView
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="com.google.android.gms.maps.MapFragment"
        />

</RelativeLayout>



MainActivity.java

import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.gun0912.tedpermission.PermissionListener;
import com.gun0912.tedpermission.TedPermission;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    Context context;
    private Button tab1, tab2, tab3;
    private Fragment fragment = null;

    PermissionListener permissionlistener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            initView();
        }

        @Override
        public void onPermissionDenied(ArrayList<String> deniedPermissions) {
            Toast.makeText(context, "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.", Toast.LENGTH_SHORT).show();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = this.getBaseContext();
        checkPermissions();
    }

    private void checkPermissions() {
        if (Build.VERSION.SDK_INT >= 23) { // 마시멜로(안드로이드 6.0) 이상 권한 체크
            TedPermission.with(context)
                    .setPermissionListener(permissionlistener)
                    .setRationaleMessage("앱을 이용하기 위해서는 접근 권한이 필요합니다")
                    .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                    .setPermissions(new String[]{
                            android.Manifest.permission.ACCESS_FINE_LOCATION,
                            android.Manifest.permission.ACCESS_COARSE_LOCATION
                            //android.Manifest.permission.READ_EXTERNAL_STORAGE,
                            //android.Manifest.permission.WRITE_EXTERNAL_STORAGE // 기기, 사진, 미디어, 파일 엑세스 권한
                    })
                    .check();

        } else {
            initView();
        }
    }

    private void initView() {
        tab1 = findViewById(R.id.btn_tab1);
        tab2 = findViewById(R.id.btn_tab2);
        tab3 = findViewById(R.id.btn_tab3);

        tab1.setOnClickListener(this);
        tab2.setOnClickListener(this);
        tab3.setOnClickListener(this);

        if(findViewById(R.id.fragment_container) != null){
            Fragment01 fragment01 = new Fragment01();
            fragment01.setArguments(getIntent().getExtras());

            getFragmentManager().beginTransaction().add(R.id.fragment_container, fragment01).commitAllowingStateLoss();
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_tab1:
                fragment = new Fragment01();
                selectFragment(fragment);
                break;
            case R.id.btn_tab2:
                fragment = new Fragment02();
                selectFragment(fragment);
                break;
            case R.id.btn_tab3:
                fragment = new Fragment03();
                selectFragment(fragment);
                break;
        }
    }

    private void selectFragment(Fragment fragment) {
        // 액티비티 내의 프래그먼트를 관리하려면 FragmentManager를 사용해야 함.
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.fragment_container,fragment);
        // FragmentTransaction을 변경하고 나면, 반드시 commit()을 호출해야 변경 내용이 적용됨
        fragmentTransaction.commit();
    }
}


Fragment2.java

package com.link2me.android.googlemap;

import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;

import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapView;
import com.google.android.gms.maps.MapsInitializer;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;

public class Fragment02 extends Fragment implements OnMapReadyCallback {

    private FragmentActivity mContext;

    private static final String TAG = Fragment02.class.getSimpleName();
    private GoogleMap mMap;
    private MapView mapView = null;
    private Marker currentMarker = null;

    // The entry point to the Fused Location Provider.
    private FusedLocationProviderClient mFusedLocationProviderClient; // Deprecated된 FusedLocationApi를 대체
    private LocationRequest locationRequest;
    private Location mCurrentLocatiion;

    private final LatLng mDefaultLocation = new LatLng(37.56, 126.97);
    private static final int DEFAULT_ZOOM = 15;
    private static final int PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 1;
    private boolean mLocationPermissionGranted;

    private static final int GPS_ENABLE_REQUEST_CODE = 2001;
    private static final int UPDATE_INTERVAL_MS = 1000 * 60 * 1;  // 1분 단위 시간 갱신
    private static final int FASTEST_UPDATE_INTERVAL_MS = 1000 * 30 ; // 30초 단위로 화면 갱신

    private static final String KEY_CAMERA_POSITION = "camera_position";
    private static final String KEY_LOCATION = "location";

    public Fragment02() {
    }

    @Override
    public void onAttach(Activity activity) { // Fragment 가 Activity에 attach 될 때 호출된다.
        mContext =(FragmentActivity) activity;
        super.onAttach(activity);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 초기화 해야 하는 리소스들을 여기서 초기화 해준다.
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        // Layout 을 inflate 하는 곳이다.
        if (savedInstanceState != null) {
            mCurrentLocatiion = savedInstanceState.getParcelable(KEY_LOCATION);
            CameraPosition mCameraPosition = savedInstanceState.getParcelable(KEY_CAMERA_POSITION);
        }
        View layout =  inflater.inflate(R.layout.fragment_02,container,false);
        mapView = (MapView)layout.findViewById(R.id.map);
        if(mapView != null) {
            mapView.onCreate(savedInstanceState);
        }
        mapView.getMapAsync(this);
        return layout;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        // Fragement에서의 OnCreateView를 마치고, Activity에서 onCreate()가 호출되고 나서 호출되는 메소드이다.
        // Activity와 Fragment의 뷰가 모두 생성된 상태로, View를 변경하는 작업이 가능한 단계다.
        super.onActivityCreated(savedInstanceState);

        //액티비티가 처음 생성될 때 실행되는 함수
        MapsInitializer.initialize(mContext);

        locationRequest = new LocationRequest()
                .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) // 정확도를 최우선적으로 고려
                .setInterval(UPDATE_INTERVAL_MS) // 위치가 Update 되는 주기
                .setFastestInterval(FASTEST_UPDATE_INTERVAL_MS); // 위치 획득후 업데이트되는 주기

        LocationSettingsRequest.Builder builder =
                new LocationSettingsRequest.Builder();

        builder.addLocationRequest(locationRequest);

        // FusedLocationProviderClient 객체 생성
        mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(mContext);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        mapView.onSaveInstanceState(outState);
    }

    @Override
    public void onMapReady(GoogleMap googleMap) {
        mMap = googleMap;

        setDefaultLocation(); // GPS를 찾지 못하는 장소에 있을 경우 지도의 초기 위치가 필요함.

        getLocationPermission();

        updateLocationUI();

        getDeviceLocation();
    }

    private void updateLocationUI() {
        if (mMap == null) {
            return;
        }
        try {
            if (mLocationPermissionGranted) {
                mMap.setMyLocationEnabled(true);
                mMap.getUiSettings().setMyLocationButtonEnabled(true);
            } else {
                mMap.setMyLocationEnabled(false);
                mMap.getUiSettings().setMyLocationButtonEnabled(false);
                mCurrentLocatiion = null;
                getLocationPermission();
            }
        } catch (SecurityException e)  {
            Log.e("Exception: %s", e.getMessage());
        }
    }

    private void setDefaultLocation() {
        if (currentMarker != null) currentMarker.remove();

        MarkerOptions markerOptions = new MarkerOptions();
        markerOptions.position(mDefaultLocation);
        markerOptions.title("위치정보 가져올 수 없음");
        markerOptions.snippet("위치 퍼미션과 GPS 활성 여부 확인하세요");
        markerOptions.draggable(true);
        markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED));
        currentMarker = mMap.addMarker(markerOptions);

        CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(mDefaultLocation, 15);
        mMap.moveCamera(cameraUpdate);
    }

    String getCurrentAddress(LatLng latlng) {
        // 위치 정보와 지역으로부터 주소 문자열을 구한다.
        List<Address> addressList = null ;
        Geocoder geocoder = new Geocoder( mContext, Locale.getDefault());

        // 지오코더를 이용하여 주소 리스트를 구한다.
        try {
            addressList = geocoder.getFromLocation(latlng.latitude,latlng.longitude,1);
        } catch (IOException e) {
            Toast. makeText( mContext, "위치로부터 주소를 인식할 수 없습니다. 네트워크가 연결되어 있는지 확인해 주세요.", Toast.LENGTH_SHORT ).show();
            e.printStackTrace();
            return "주소 인식 불가" ;
        }

        if (addressList.size() < 1) { // 주소 리스트가 비어있는지 비어 있으면
            return "해당 위치에 주소 없음" ;
        }

        // 주소를 담는 문자열을 생성하고 리턴
        Address address = addressList.get(0);
        StringBuilder addressStringBuilder = new StringBuilder();
        for (int i = 0; i <= address.getMaxAddressLineIndex(); i++) {
            addressStringBuilder.append(address.getAddressLine(i));
            if (i < address.getMaxAddressLineIndex())
                addressStringBuilder.append("\n");
        }

        return addressStringBuilder.toString();
    }

    LocationCallback locationCallback = new LocationCallback() {
        @Override
        public void onLocationResult(LocationResult locationResult) {
            super.onLocationResult(locationResult);

            List<Location> locationList = locationResult.getLocations();

            if (locationList.size() > 0) {
                Location location = locationList.get(locationList.size() - 1);

                LatLng currentPosition
                        = new LatLng(location.getLatitude(), location.getLongitude());

                String markerTitle = getCurrentAddress(currentPosition);
                String markerSnippet = "위도:" + String.valueOf(location.getLatitude())
                        + " 경도:" + String.valueOf(location.getLongitude());

                Log.d(TAG, "Time :" + CurrentTime() + " onLocationResult : " + markerSnippet);

                //현재 위치에 마커 생성하고 이동
                setCurrentLocation(location, markerTitle, markerSnippet);
                mCurrentLocatiion = location;
            }
        }

    };

    private String CurrentTime(){
        Date today = new Date();
        SimpleDateFormat date = new SimpleDateFormat("yyyy/MM/dd");
        SimpleDateFormat time = new SimpleDateFormat("hh:mm:ss a");
        return time.format(today);
    }

    public void setCurrentLocation(Location location, String markerTitle, String markerSnippet) {
        if (currentMarker != null) currentMarker.remove();

        LatLng currentLatLng = new LatLng(location.getLatitude(), location.getLongitude());

        MarkerOptions markerOptions = new MarkerOptions();
        markerOptions.position(currentLatLng);
        markerOptions.title(markerTitle);
        markerOptions.snippet(markerSnippet);
        markerOptions.draggable(true);

        currentMarker = mMap.addMarker(markerOptions);

        CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLng(currentLatLng);
        mMap.moveCamera(cameraUpdate);
    }

    private void getDeviceLocation() {
        try {
            if (mLocationPermissionGranted) {
                mFusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper());
            }
        } catch (SecurityException e)  {
            Log.e("Exception: %s", e.getMessage());
        }
    }

    private void getLocationPermission() {
        if (ContextCompat.checkSelfPermission(mContext,
                android.Manifest.permission.ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED) {
            mLocationPermissionGranted = true;
        } else {
            ActivityCompat.requestPermissions(mContext,
                    new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION},
                    PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String permissions[],
                                           @NonNull int[] grantResults) {
        mLocationPermissionGranted = false;
        switch (requestCode) {
            case PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION: {
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    mLocationPermissionGranted = true;
                }
            }
        }
        updateLocationUI();
    }


    public boolean checkLocationServicesStatus() {
        LocationManager locationManager = (LocationManager) getActivity().getSystemService(Context.LOCATION_SERVICE);

        return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
                locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
    }

    @Override
    public void onStart() { // 유저에게 Fragment가 보이도록 해준다.
        super.onStart();
        mapView.onStart();
        Log.d(TAG, "onStart ");
    }

    @Override
    public void onStop() {
        super.onStop();
        mapView.onStop();
        if (mFusedLocationProviderClient != null) {
            Log.d(TAG, "onStop : removeLocationUpdates");
            mFusedLocationProviderClient.removeLocationUpdates(locationCallback);
        }
    }

    @Override
    public void onResume() { // 유저에게 Fragment가 보여지고, 유저와 상호작용이 가능하게 되는 부분
        super.onResume();
        mapView.onResume();
        if (mLocationPermissionGranted) {
            Log.d(TAG, "onResume : requestLocationUpdates");
            mFusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, null);
            if (mMap!=null)
                mMap.setMyLocationEnabled(true);
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        mapView.onPause();
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        mapView.onLowMemory();
    }

    @Override
    public void onDestroyView() { // 프래그먼트와 관련된 View 가 제거되는 단계
        super.onDestroyView();
        if (mFusedLocationProviderClient != null) {
            Log.d(TAG, "onDestroyView : removeLocationUpdates");
            mFusedLocationProviderClient.removeLocationUpdates(locationCallback);
        }
    }

    @Override
    public void onDestroy() {
        // Destroy 할 때는, 반대로 OnDestroyView에서 View를 제거하고, OnDestroy()를 호출한다.
        super.onDestroy();
        mapView.onDestroy();
    }

}


테스트한 소스 코드는 다른 기능을 좀 더 구현 테스트해보고 올려둘 생각이다.


블로그 이미지

Link2Me

,