Android 채팅 라이브러리를 추가하면 에러가 발생한다.

implementation('com.github.nkzawa:socket.io-client:0.5.0') {
    exclude group: 'org.json', module: 'json'
}

원인을 알고보니 targetSdkVersion 26 에서는 com.github.nkzawa:socket.io-client:0.3.0 으로 하면 정상 동작한다.
com.github.nkzawa:socket.io-client:0.5.0 은 targetSdkVersion 27 에서 정상 동작하더라.

Creating a realtime chat app with android , NodeJs and Socket.io 에 대한 최신 예제는 https://dev.to/medaymentn/creating-a-realtime-chat-app-with-android--nodejs-and-socketio-4o55 를 참조하여 실행하면 Node.js 와 안드로이드간 통신이 간단하게 된다는 걸 확인할 수 있다.

회원정보와 연동하고, 내가 보낸 메시지, 상대방이 보낸 메시지를 구분하는 것은 안된다.

회원정보 연동은 Android-PHP-MySQL 통신으로 획득한 정보를 Node.js-Android 간에 활용하면 된다.
내가 보낸 메시지와 상대방이 보낸 메시지 구분은 RecyclerView 기능을 활용하면 해결 할 수 있다.


728x90
블로그 이미지

Link2Me

,

안드로이드 Fragment 예제를 간략하게 정리했다.

API Level 11(Android 3.0) 이상에서 지원한다. 요즈음에는 최소 4.4 이상으로 컴파일을 하므로 기본적으로 제공된다고 볼 수 있다.


Fragment를 사용하는 가장 큰 목적은 분할된 화면들을 독립적으로 구성하고 그 상태를 관리하는데 있다.

Fragment는 항상 Activity 위에 올라가 있어야 한다.

Activity 로 만들어진 화면을 분할하여 각각의 부분화면을 Fragment로 만들고 그 Fragment를 독립적으로 관리하는 것을 목표로 한다.


Fragment는 항상 Activity 내에 포함되어 있어야 하며 해당 Fragment의 수명 주기는 호스트 Activity의 수명 주기에 직접적으로 영향을 받는다.
예를 들어 Activity가 일시정지되는 경우, 그 안의 모든 Fragment도 일시정지되며 Activity가 소멸되면 모든 Fragment도 마찬가지로 소멸된다. 그러나 Activity가 실행 중인 동안에는 각 Fragment를 추가 또는 제거하는 등 개별적으로 조작할 수 있다.


Activity 의 UI를 보여주기 위해서 onCreate 메소드를 Override 하고 Layout Resource ID인 R.layout.activity_main을 파라미터로 넘기면서 setContentView 메소드를 호출한다.


먼저, MainActivity 에 표시될 Layout XML 구조는 아래에 버튼을 클릭하면 Fragment 화면이 변경되도록 구성했다.


<?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">

    <LinearLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        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="Tab 1"/>

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

        <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_container 에 표시될 Fragment XML 은 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    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">

    <TextView
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="프래그먼트 1" />

</RelativeLayout>

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    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:background="#ffe4e1">

    <TextView
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="프래그먼트 2" />

</RelativeLayout>

<?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:orientation="vertical"
    android:background="#81DAF5" >

    <TextView
        android:id="@+id/fr_tv03"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:layout_centerInParent="true"
        android:text="프래그먼트 3"
        android:textStyle="bold"/>

</RelativeLayout>


Fragment01.java

Alt + Insert 키를 눌러서 아래 메소드를 추가한 다음에 view를 return해 주도록 코드를 수정한다.

Fragment는 Fragment Class 를 상속하여 만들 수 있다.

 public class Fragment01 extends Fragment {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        // onCreateView() : 프래그먼트와 연관된 뷰 계층을 생성하기 위해 호출됨
        // Inflate the layout for this fragment
        ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.fragment_01, container, false);
        return rootView;
        //return super.onCreateView(inflater, container, savedInstanceState); // Alt + Insert키 누르면 자동 생성되는 return
    }
}


Fragment02.java

- Activity 에서 전달한 데이터를 받아서 화면에 표시하기

- Fragment 로 데이터 전달법은 구글에서 Passing data to activity and fragment in Android 로 검색하면 된다.

- onCreateView 에서 Activity 에서 전달한 데이터를 받아도 된다.

public class Fragment02 extends Fragment {
    TextView textView;
    String strtext;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            strtext = getArguments().getString("edttext"); // Activity 에서 받은 데이터
        }
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.fragment_02, container, false);
        textView = (TextView) rootView.findViewById(R.id.fr_tv02);
        textView.setText(strtext);
        return rootView;
        //return super.onCreateView(inflater, container, savedInstanceState);
    }
}


Fragment03.java

 public class Fragment03 extends Fragment {
    TextView textView;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        // onCreateView()로 전달된 container 매개변수가 상위 ViewGroup 이며(액티비티의 레이아웃으로부터), 이 안에 프래그먼트 레이아웃이 삽입
        // savedInstanceState 매개변수는 Bundle이며, 프래그먼트가 재개되는 중에 프래그먼트의 이전 인스턴스에 대한 데이터를 제공
        // Inflate the layout for this fragment
        // Inflate 시키고자 하는 Layout 의 Resource ID, Inflated 된 Layout 의 상위가 될 ViewGroup
        ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.fragment_03, container, false);
        textView = (TextView) rootView.findViewById(R.id.fr_tv03);
        textView.setText("Fragment 3을 선택했네요.");
        return rootView;
    }
}


MainActivity.java

Activity에서 Fragment를 불러오는 기능을 수행

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

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

        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){
            if(savedInstanceState != null) return;
            Fragment01 fragment01 = new Fragment01();
            fragment01.setArguments(getIntent().getExtras());

            FragmentManager fragmentManager = getFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            // add() 메서드를 사용하여 프래그먼트를 추가하고, 추가할 프래그먼트와 이를 삽입할 뷰를 지정
            fragmentTransaction.add(R.id.fragment_container, fragment01);
            // FragmentTransaction을 변경하고 나면, 반드시 commit()을 호출해야 변경 내용이 적용됨
            fragmentTransaction.commit();
        }
        // 상세 설명은 https://developer.android.com/guide/components/fragments?hl=ko 참조
    }


    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_tab1:
                fragment = new Fragment01();
                changeFragment(fragment);
                break;
            case R.id.btn_tab2:
                Bundle bundle = new Bundle();
                bundle.putString("edttext", "From Activity");
                fragment = new Fragment02();
                fragment.setArguments(bundle); // Fragment 로 데이터 전달
                changeFragment(fragment);
                break;
            case R.id.btn_tab3:
                fragment = new Fragment03();
                changeFragment(fragment);
                break;
        }
    }

    private void changeFragment(Fragment fragment) {
        // 프로그래밍 방식으로 프래그먼트를 기존의 ViewGroup에 추가
        // 액티비티가 실행 중인 동안에는 언제든 액티비티 레이아웃에 프래그먼트를 추가 가능
        // 액티비티 내의 프래그먼트를 관리하려면 FragmentManager를 사용해야 함.
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.fragment_container,fragment);
        //fragmentTransaction.addToBackStack(null); // back 버튼을 눌렀을 때 동작하는 옵션.
        fragmentTransaction.commit(); // 트랜잭션을 액티비티에 적용하려면 반드시 commit()을 호출해야 함.
    }
}


Fragment는 Activity가 아니기 때문에 context를 갖지 않는다.
Fragment 구현시 context를 이용해야 할 경우 getActivity()함수를 이용한다.
getActivity().getApplicationContext();
getActivity().finish();
getActivity().getSharedPreferences("pref", Activity.MODE_PRIVATE);
getActivity().getSystemService(Context.USB_SERVICE);
getActivity().getSystemService(getActivity().CLIPBOARD_SERVICE);


테스트 파일

src.zip


사양이 낮은 폰에서 테스트를 못해봤는데 만약 동작되지 않는다면, import 수정.

http://www.androhub.com/android-pass-data-from-activity-to-fragment/ 에 나오는 import 참조하면 될 듯....


참조하면 도움되는 게시글

http://family-gram.tistory.com/60


728x90

'안드로이드 > Layout' 카테고리의 다른 글

ScrollView  (0) 2019.12.20
Dynamic Layouts  (0) 2019.01.06
FloatingActionButton(FAB)  (0) 2018.08.15
LinearLayout weight  (0) 2018.03.01
Android ViewFlipper(뷰플리퍼)  (0) 2017.05.02
블로그 이미지

Link2Me

,

AsyncTask 를 Class 화하여 사용하는 방법이다.


private void getJSONFromAsyncHttpClient() {
    Uri.Builder builder = new Uri.Builder()
            .appendQueryParameter("uID", Value.encrypt(Value.getPhoneUID(context)))
            .appendQueryParameter("userID", Value.encrypt(userID))
            .appendQueryParameter("searName", Value.encrypt(gubun))
            .appendQueryParameter("searValue", Value.encrypt(tmpStr))
            .appendQueryParameter("keyword", Value.encrypt(Value.URLkey()));
    String params = builder.build().getEncodedQuery();

    AsyncHttpComm asyncHttpComm = new AsyncHttpComm();
    try {
        String result = asyncHttpComm.execute(Value.IPADDRESS + "/SearchList.php", params).get();
        showJSONList(result);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
} 


위 코드를 보면 AsyncHttpComm Class를 호출하고 결과 문자열을 String result 로 받고 있다.

그리고 showJSONList(result) 에 결과를 넣어서 서버로 받은 메시지를 파싱하여 RecyclerView 에서 처리하면 된다.


그럼 이제 AsyncHttpComm Class 는 어떻게 되어 있는지 보자.

public class AsyncHttpComm extends AsyncTask<String, Void, String> {

    @Override
    protected String doInBackground(String... params) {
        try {
            return PHPComm.getJson(params[0], params[1]);
        } catch (Exception e) {
            return new String("Exception: " + e.getMessage());
        }
    }
} 


String 변수를 받아서 백그라운드 처리하고, String 결과를 받고 있다.


또 메소드 처리되어 있는 PHPComm Class 는 어떻게 만들어져 있나 보자.

세션에 대한 내용이 적혀 있는데 별 의미가 없는 거 같다.

내가 아직 잘 몰라서 그런지 안드로이드와 PHP 간 통신하는데 있어서 세션값을 물고 다니면서 처리하는 방식으로 구현하지 않아도 통신하는데 전혀 문제가 되지 않는다.

스마트폰 고유장치 값, 키 값을 이용하여 PHP 에서 해킹시도 자체를 방지할 수 있다.


public class PHPComm extends Activity {

    // serverURL : JSON 요청을 받는 서버의 URL
    // postParams : POST 방식으로 전달될 입력 데이터
    // 반환 데이터 : 서버에서 전달된 JSON 데이터
    public static String getJson(String serverUrl, String postParams) throws Exception {

        BufferedReader bufferedReader = null;
        try {  
            Thread.sleep(100);
            URL url = new URL(serverUrl);  
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            // 세션 쿠키 전달
            String cookieString = CookieManager.getInstance().getCookie(Value.IPADDRESS);
            
            StringBuilder sb = new StringBuilder();  

            if(conn != null){ // 연결되었으면
                //add request header
                conn.setRequestMethod("POST");
                conn.setRequestProperty("USER-AGENT", "Mozilla/5.0");
                conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                conn.setRequestProperty("Accept-Language", "en-US,en;q=0.5");
                if (cookieString != null) {
                   conn.setRequestProperty("Cookie", cookieString);
                   Log.e("PHP_getCookie", cookieString);
                 }
                conn.setConnectTimeout(10000);
                conn.setReadTimeout(10000);
                conn.setUseCaches(false);
                conn.setDefaultUseCaches(false);
                conn.setDoOutput(true); // POST 로 데이터를 넘겨주겠다는 옵션
                conn.setDoInput(true);

                // Send post request
                DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
                wr.writeBytes(postParams);
                wr.flush();
                wr.close();

                int responseCode = conn.getResponseCode();
                System.out.println("GET Response Code : " + responseCode);        
                if(responseCode == HttpURLConnection.HTTP_OK){ // 연결 코드가 리턴되면
                    bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                    String json;
                    while((json = bufferedReader.readLine())!= null){
                        sb.append(json + "\n");
                    }      
                }
                bufferedReader.close();
            }
            System.out.println("PHP Comm Out : " + sb.toString());
            return sb.toString().trim();  
            // 수행이 끝나고 리턴하는 값은 다음에 수행될 onProgressUpdate 의 파라미터가 된다
        } catch(Exception e){  
            return new String("Exception: " + e.getMessage());
        }   
 
    }
}


이 두개의 Class 를 만들고 나면 Library 를 사용하는 것만큼 편하게 Text 데이터를 수신하여 파싱하고 결과처리를 쉽게 할 수 있다.


http://www.masterqna.com/android/29387/asynctask-%EC%9D%98-get-%EB%A9%94%EC%86%8C%EB%93%9C-%EC%A7%88%EB%AC%B8

에 나온 내용을 보니

AsyncTask.get()은 UI thread를 block 한다고
AsyncTaks안에 onPostExecute()를 implement하고 Handle 하라고 나온다.
시간이 오래 걸리는 작업은 AsyncTask.get()을 사용하지 말자.



이런거 귀찮다?????

좀 더 간편하게 라이브러리 이용하여 하고 싶다면

앱 build.gradle 에 아래 라이브러리를 추가한다.

dependencies {
    implementation 'com.loopj.android:android-async-http:1.4.9'
}


코드는 아래와 같이 심플하다.


 private void getJSONFromAsyncHttpClient() {
    RequestParams params = new RequestParams();
    params.put("keyword", Value.encrypt(Value.URLkey()));
    params.put("uID", Value.encrypt(Value.getPhoneUID(context)));
    params.put("userID", Value.encrypt(userID));
    params.put("searName", Value.encrypt(gubun));
    params.put("searValue", Value.encrypt(tmpStr));

    AsyncHttpClient client = new AsyncHttpClient();
    client.post(Value.IPADDRESS + "/SearchList.php", params, new TextHttpResponseHandler() {
        @Override
        public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
            System.out.println("statusCode : " + statusCode);
            Toast.makeText(Search.this, "서버와의 통신 중 에러가 발생했습니다.", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onSuccess(int statusCode, Header[] headers, String responseString) {
            System.out.println("statusCode : " + statusCode);
            //System.out.println("responseString : " + responseString);
            showJSONList(responseString);
        }
    });
}


라이브러리가 모든 폰에서 잘 동작되는지 여부를 확인한 것은 아니라서 표준 HttpUrlConnection 을 이용하여 통신하는 걸 구현하여 사용중이다.

Asynchronous HTTP Client 라이브러리에 대한 내용은 http://link2me.tistory.com/1493 참조하면 된다.


도움이 되셨다면 공감 그리고 ... 클릭 해 주시면 큰 힘이 됩니다.

728x90
블로그 이미지

Link2Me

,

Android background 실행 제한

https://developer.android.com/about/versions/oreo/background?hl=ko 의 내용을 발췌 요약했다.


Android 8.0는 사용자 경험을 개선하기 위해 백그라운드에서 실행되면서 앱의 동작을 제한한다.
잘 동작하던 발신자정보 팝업창이 Android 8.0 에서 잠시 보여주고 사라져 버린다.
- 백그라운드 서비스 제한: 앱이 유휴 상태인 경우 백그라운드 서비스의 사용이 제한된다.
  마치 앱이 서비스의 Service.stopSelf() 메서드를 호출한 것처럼 시스템이 앱의 백그라운드 서비스를 중지시킨다.
  . 액티비티가 시작되거나 일시 중지되거나 상관없이 보이는 액티비티가 있는 경우 앱이 포그라운드에 있는 것으로 간주
  . 포그라운드 서비스가 있는 경우 앱이 포그라운드에 있는 것으로 간주
- 브로드캐스트 제한: 앱이 암시적 브로드캐스트에 등록하기 위해 자체 매니페스트를 사용할 수 없다.
  그렇지만 여전히 앱이 런타임에 브로드캐스트에 등록할 수 있으며,
  특정 앱을 대상으로 하는 명시적 브로드캐스트에 등록하기 위해 매니페스트를 사용할 수 있다.
앱은 JobScheduler 작업을 사용하여 이러한 제한을 해결할 수 있다.
Android 8.0에서는 JobScheduler에 대한 여러 가지 개선 사항을 제공하며 이를 통해 서비스와 브로드캐스트 수신기를 예약된 작업으로 쉽게 바꿀 수 있다.

앱이 허용 목록에 있는 동안에는 제한없이 서비스를 시작
- 우선순위가 높은 Firebase 클라우드 메시징(FCM) 메시지 처리
- SMS/MMS 메시지와 같은 브로드캐스트 수신
- 알림에서 PendingIntent 실행

Android 8.0에서는 새 서비스를 포그라운드에서 시작하는 새로운 메서드 Context.startForegroundService()를 소개
- 시스템이 서비스를 생성한 후, 앱은 5초 이내에 해당 서비스의 startForeground() 메서드를 호출하여 새 서비스의 알림을 사용자에게 표시해야 한다.
- 앱이 이 시간 한도 내에 startForeground()를 호출하지 않으면 시스템이 서비스를 중단하고 이 앱을 ANR로 선언한다.
- 오디오를 재생하는 서비스는 항상 포그라운드 서비스여야 한다.
  startService() 대신 NotificationManager.startServiceInForeground()를 사용하여 서비스를 생성한다.


- Foreground service 의 경우 반드시 noficiation 을 제공해야 한다.
  Foreground service 는 보통 Music Player 와 같이 interactive 한 service 에 사용한다.
  foreground 는 startForeground() 로 시작시킨다. 이 때 Notification ID 와 notification을 함께 전달한다.
  parameter로 stopForeground() 로 stop 시키는데 parameter 로 notification 도 함께 제거할지를 boolean 값으로 넘기게 된다.

// 서비스를 실행할 때
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    context.startForegroundService(new Intent(context, ServedService.class));
} else {
    context.startService(new Intent(context, ServedService.class));
}

// 서비스 파일 내에서
@Override
public void onCreate() {
    super.onCreate();
    startForeground(1,new Notification()); // 추가 (9.0에서는 변경해야 함)
}

@Override
public void onDestroy() {
    // 서비스가 종료될 때 실행
    super.onDestroy();
    stopForeground(true);
}


2019.4.6일 추가 내용


Android 9.0 에서는 아래와 같이 변경해야 제대로 동작한다.

AndroidManifest.xml 파일에 한줄 추가해야 한다.

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


startForeground(1,new Notification()); // 대신에 아래 코드로 변경한다.


        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
            startMyOwnForeground();

    @RequiresApi(api = Build.VERSION_CODES.O)
    private void startMyOwnForeground() {
        String NOTIFICATION_CHANNEL_ID = "com.link2me.android.Phone";
        String channelName = "Incoming Calling Service";
        NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE);
        chan.setLightColor(Color.BLUE);
        chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
        NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        assert manager != null;
        manager.createNotificationChannel(chan);

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID);
        Notification notification = notificationBuilder.setOngoing(true)
                .setSmallIcon(R.drawable.icon)
                .setContentTitle("App is running in background")
                .setPriority(NotificationManager.IMPORTANCE_MIN)
                .setCategory(Notification.CATEGORY_SERVICE)
                .build();
        startForeground(2, notification);
    }
 


전화번호 액세스 제한

https://developer.android.com/about/versions/pie/android-9.0-changes-all?hl=ko




728x90
블로그 이미지

Link2Me

,

안드로이드 8.0 에서 서비스를 실행할 때 아래코드를 추가해줘야 에러가 발생하지 않는다.


// 서비스를 실행할 때
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    context.startForegroundService(new Intent(context, ServedService.class));
} else {
    context.startService(new Intent(context, ServedService.class));
}

// 서비스 파일 내에서
@Override
public void onCreate() {
    super.onCreate();
    startForeground(1,new Notification()); // 추가
}

@Override
public void onDestroy() {
    // 서비스가 종료될 때 실행
    super.onDestroy();
    stopForeground(true);
}



728x90
블로그 이미지

Link2Me

,

안드로이드 Notification Helper 자료를 검색하고 내용을 보강하고 적어둔다.

안드로이드 8.0 이상(targetSdkVersion 26 이상)에서도 동작되는 코드다.

Noti 메시지를 띄우는createNotification(String title, String message) 과 클릭하면 cancelNotification(Context ctx, int notifyId) 되는 걸 테스트했다.

핵심은 NotificationHelper Class 만드는 것이고 나머지는 간단하게 사용해본 것이다.


 public class NotificationHelper {
    private Context mContext;
    private NotificationManager notificationManager;
    public static final String NOTIFICATION_CHANNEL_ID = "10001";

    public NotificationHelper(Context context) {
        mContext = context;
    }

    /**
     * Create and push the notification
     */
    public void createNotification(String title, String message) {
        Intent resultIntent = new Intent(mContext , ShowNotification.class);
        resultIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);

        PendingIntent resultPendingIntent = PendingIntent.getActivity(mContext,
                0 /* Request code */, resultIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mContext,NOTIFICATION_CHANNEL_ID);
        mBuilder.setSmallIcon(R.mipmap.ic_launcher);
        mBuilder.setContentTitle(title)
                .setContentText(message)
                .setAutoCancel(false) //클릭하게 되면 사라지도록...
                .setSound(Settings.System.DEFAULT_NOTIFICATION_URI)
                .setVibrate(new long[] { 1000, 1000, 1000 }) //노티가 등록될 때 진동 패턴
                .setContentIntent(resultPendingIntent);

        notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){ // 8.0 이상
            int importance = NotificationManager.IMPORTANCE_HIGH;
            String channelName = "NOTIFICATION_CHANNEL_NAME";
            NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance);
            notificationChannel.enableLights(true);
            notificationChannel.setLightColor(Color.RED);
            notificationChannel.enableVibration(true);
            notificationChannel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
            assert notificationManager != null;
            mBuilder.setChannelId(NOTIFICATION_CHANNEL_ID);
            notificationManager.createNotificationChannel(notificationChannel);
        }
        assert notificationManager != null;
        notificationManager.notify(0 /* Request Code */, mBuilder.build());
    }

    public void callSettingNotification(){
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
            intent.putExtra(Settings.EXTRA_APP_PACKAGE,mContext.getPackageName());
            intent.putExtra(Settings.EXTRA_CHANNEL_ID,NOTIFICATION_CHANNEL_ID);
            mContext.startActivity(intent);
        }
    }

    public void cancelNotification(Context ctx, int notifyId) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){
            NotificationManager mNotificationManager =
                    (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
            mNotificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
        } else {
            NotificationManager notificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
            notificationManager.cancel(notifyId);
        }
    }
}


테스트에 사용한 파일을 첨부

notificationmanager.zip


PendingIntent

PendingIntent는 Intent를 포함하는 인텐트로, 목적은 본인 앱이 아닌 외부 앱(Notification, Alarm 등)에게 권한을 줘서 본인 앱을 실행할 수 있도록 허락하는 것이다.


A PendingIntent is a token that you give to a foreign application (e.g. NotificationManager, AlarmManager, Home Screen AppWidgetManager, or other 3rd party applications), which allows the foreign application to use your application's permissions to execute a predefined piece of code.


pending : 미결의, ~를 기다리는 동안


Intent의 기본 개념은 특정 컴포넌트(Activity, Service, Broadcast Receiver, Content Provider)를 실행시키는 메시지라는 것이다.

Intent intent = new Intent(MainActivity.this, NewActivity.class);

startActivity(intent);

그런데 PendingIntent는 생성자가 없고 다음 세 개의 메소드들에 의해서 객체가 생성된다.
ㆍgetActivity(Context, int, Intent, int)
ㆍgetBroadcast(Context, int, Intent, int)
ㆍgetService(Context, int, Intent, int)

- 액티비티를 시작할 인텐트를 생성하기 위해선 PendingIntent.getActivity()를 사용한다.
- 서비스를 시작할 인텐트를 생성하기 위해서는 PendingIntent.getService()를 사용한다.
- BroadcastReceiver를 시작할 인텐트를 생성하기 위해서는 PendingIntent.getBroadcast()를 사용한다.


Intent intent = new Intent(context, MainActivity.class);
PendingIntent pendIntent = PendingIntent.getActivity(context, 0 , intent, PendingIntent.FLAG_UPDATE_CURRENT);

- 정상적인 Activity lifecycle을 태우기 위해선, PendingIntent 생성시 requestCode를 반드시 넣어야한다.(대부분 default값으로 0을 넣는다)

- FLAG_CANCEL_CURRENT : 이전에 생성한 PendingIntent는 취소하고 새로 만든다.
- FLAG_IMMUTABLE : 생성된 PendingIntent 는 수정 불가능하도록 한다.
- FLAG_NO_CREATE : 생성된 PendingIntent 를 반환한다.(재사용 가능), 이미 생성된 PendingIntent가 없다면 null를 return 한다.
- FLAG_ONE_SHOT : 해당 Flag로 생성한 PendingIntent 는 일회성이다.
- FLAG_UPDATE_CURRENT : 이미 생성된 PendingIntent가 존재하면 해당 Intent의 Extra Data만 변경한다.


알람 확인 및 취소 게시글 : https://migom.tistory.com/10





참고사이트

https://medium.com/cr8resume/notification-in-android-8-0-oreo-implementing-notification-channels-d65b0f81ca50


https://www.androidauthority.com/android-8-0-oreo-app-implementing-notification-channels-801097/


http://gun0912.tistory.com/77

728x90
블로그 이미지

Link2Me

,



안드로이드 스튜디오에서 Interface CallbackEvent를 생성하고 나서 EventRegistration Class 에서 CallbackEvent  변수를 선언하고 생성자 만들기를 하면 자동으로 생성자가 만들어지는 걸 볼 수 있다.


public interface CallbackEvent {
    // 1 Step 인터페이스 정의
    public void callbackMethod();
}

 public class EventRegistration {
    // 2 Step 변수 선언
    CallbackEvent callbackEvent;

    // 3 Step 생성자 생성
    public EventRegistration(CallbackEvent callbackEvent) {
        System.out.println("2. EventRegistration Constructor");
        this.callbackEvent = callbackEvent;
    }

    public void doWork(){
        System.out.println("4. doWork 메소드");
        callbackEvent.callbackMethod();
    }
}

 public class MainActivity extends AppCompatActivity {

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

        System.out.println("1. MainActivity");
        CallbackEvent callbackEvent = new CallbackEvent() {
            @Override
            public void callbackMethod() {
                System.out.println("5. call callback method from callee");
            }
        };

        EventRegistration eventRegistration = new EventRegistration(callbackEvent);
        System.out.println("3. eventRegistration.doWork()");
        eventRegistration.doWork();
    }
}


생성자 생성과 동시에 EventRegistration(CallbackEvent callbackEvent) 를 지정하는 방법과

별도 setCallbackEvent(CallbackEvent callbackEvent) 를 하는 방법으로 구분해서 해보고 있다.


public interface CallbackEvent {
    // 1 Step 인터페이스 정의
    public void callbackMethod();
}

 public class EventRegistration {
    Context mContext;
    // 2 Step 변수 선언
    CallbackEvent callbackEvent;

    // 3 Step 생성자 선언
    public EventRegistration(Context context) {
        System.out.println("3. EventRegistration Constructor");
        mContext = context;
    }

    // 4 Step CallbackEvent Set
    public void setCallbackEvent(CallbackEvent callbackEvent){
        System.out.println("5. setCallbackEvent");
        this.callbackEvent = callbackEvent;
    }

    public void doWork(){
        System.out.println("7. doWork 메소드 응답");
        callbackEvent.callbackMethod();
    }
}

 public class MainActivity extends AppCompatActivity {
    Context context;

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

        System.out.println("1. MainActivity 실행");

        System.out.println("2. EventRegistration 생성자 호출");
        EventRegistration eventRegistration = new EventRegistration(context);

        System.out.println("4. setCallbackEvent 연동");
        eventRegistration.setCallbackEvent(new CallbackEvent() {
            @Override
            public void callbackMethod() {
                System.out.println("8. call callback method from callee");
            }
        });

        System.out.println("6. eventRegistration.doWork() 실행");
        eventRegistration.doWork();
    }
}


예제3.

안드로이드에서 보편적으로 사용되는 예제이다.

public interface CallbackEvent {
    void callbackMethod();
}

import android.content.Context;

public class NewClass {
    private Context mContext;
    private CallbackEvent callbackEvent;

    public NewClass(Context mContext) {
        System.out.println("2. NewClass Constructor");
        this.mContext = mContext;
    }

    public void setCallbackEvent(CallbackEvent callbackEvent){
        System.out.println("3. setCallbackEvent");
        this.callbackEvent = callbackEvent;
    }

    public void doWork(){
        //Do somthing...

        //call back main
        callbackEvent.callbackMethod();
        System.out.println("5. doWork 메소드 응답");
    }
}

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

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity implements CallbackEvent {
    Context mContext;
    NewClass newClass;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = MainActivity.this;
        System.out.println("1. MainActivity 실행");
        doSomething();
    }

    private void doSomething(){
        newClass = new NewClass(mContext);
        newClass.setCallbackEvent(this);
        newClass.doWork();
        System.out.println("6. newClass.doWork()");
    }

    @Override
    public void callbackMethod() {
        System.out.println("4. callbackMethod from callee.");
    }
}



728x90
블로그 이미지

Link2Me

,

초급 단계를 벗어나고자 Interface 에 대해 공부하고 있는데 생각만큼 이해가 잘 안된다.



https://guides.codepath.com/android/Creating-Custom-Listeners 를 보고 테스트 해본 걸 적어둔다.


import android.view.View;

class MyCustomObject {
    // Step 1 - 인터페이스 정의
    public interface MyCustomObjectListener {
        // These methods are the different events and
        // need to pass relevant arguments related to the event triggered
        public void onObjectReady(String title);
        // or when data has been loaded
        void onItemClick(View view, int position);
    }

    // Step 2 - This variable represents the listener passed in by the owning object
    // The listener must implement the events interface and passes messages up to the parent.
    private MyCustomObjectListener listener;

    // Constructor where listener events are ignored
    public MyCustomObject() {
        // set null or default listener or accept as argument to constructor
        this.listener = null;
    }

    // Step 3 - Assign the listener implementing events interface that will receive the events
    public void setCustomObjectListner(MyCustomObjectListener listner){
        this.listener = listner;
    }
}


import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

public class MyParentActivity extends AppCompatActivity {

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

        // Create the custom object
        MyCustomObject childObject = new MyCustomObject();

        // Step 4 - Setup the listener for this object
        childObject.setCustomObjectListner(new MyCustomObject.MyCustomObjectListener() {
            @Override
            public void onObjectReady(String title) {

            }

            @Override
            public void onItemClick(View view, int position) {

            }

        });
    }
}




Recyclerview 에서 Apdater를 만들어서 Interface 를 통해 데이터를 주고 받은 걸 해보고 싶은데 이해를 못한 부분이 있는지 예제를 보고 따라해보고 막상 적용해보려고 하면 막힌다.


public void setOnItemClickListener(OnItemClickListener listener){
   this.listener = listener;
}

를 통해서 데이터를 주고 받은 걸 해보고 싶은데 전혀 동작이 안된다.


생성자에 값을 넘겨서 처리하면 동작되는데 분리해서 접근하면 안되네.

뭔가 가능한 방법이 있을 거 같은데 실력이 미천해서 완벽한 해결을 못하고 Inner Class 로 처리를 하고 있다.

아래와 같이 생성자를 통해서 값을 넘기면 원하는 결과를 얻을 수는 있다.


public class StaffListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>  {
    public interface OnItemClickListener {
        void onItemClick(View view, int position);
    }

    public OnItemClickListener listener;

    public StaffListAdapter(Context mContext, ArrayList<Group_Item> items, OnItemClickListener listener) {
        this.mContext = mContext;
        IvList = items;
        this.listener = listener;
    }

}

public class StaffList extends AppCompatActivity {
    Context context;
    private ArrayList<Group_Item> staffItemList = new ArrayList<>();
    RecyclerView.Adapter staffListAdapter;

    staffListAdapter = new StaffListAdapter(this, staffItemList, new StaffListAdapter.OnItemClickListener() {
        @Override
        public void onItemClick(View view, int position) {
            if(staffItemList.get(position).getIsFolder().equals("1")){
            } else if(staffItemList.get(position).getIsFolder().equals("0")){
            }
        }
    });
    listView.setAdapter(staffListAdapter); // 어댑터를 리스트뷰에 세팅
}

728x90
블로그 이미지

Link2Me

,

서버에 있는 이미지를 다운로드하여 Glide 로딩 라이브러리를 이용하여 ImageView 에 보여주는 코드를 테스트하고 적어둔다.


앱 build.gradle

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    implementation 'com.github.bumptech.glide:glide:3.8.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
}



public class MainActivity extends AppCompatActivity {
    Context context;
    String croppedFolderName = "/DCIM/PhotoTemp";
    File outputFile = null;
    String outputFileName="myprofile.jpg";
    private ImageView imageView;

    private BackPressHandler backPressHandler;
    DownloadFileFromURL downloadFileAsyncTask;
    ProgressBar progressBar;

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

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

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

        backPressHandler = new BackPressHandler(this); // 뒤로 가기 버튼 이벤트

        NonSecretApp_Setting(); // 출처를 알 수 없는 앱 설정 화면 띄우기
        checkPermissions();
    }

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

        } else {
            initView();
        }
    }

    private void initView() {
        imageView = (ImageView) findViewById(R.id.cropImageView);
        Button btn_downloadPhoto = (Button) findViewById(R.id.btn_PhotoDownload);
        btn_downloadPhoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (NetworkHelper.checkConnection(context)) { // 인터넷 연결 체크
                    Log.d("Photo", "Photo Upload Task Start");
                    String PhotoURL = Value.IPADDRESS + "/myphoto.jpg";
                    downloadFileAsyncTask = new DownloadFileFromURL();
                    downloadFileAsyncTask.execute(PhotoURL);
                } else {
                    Toast.makeText(context, "인터넷 연결을 확인하세요", Toast.LENGTH_LONG).show();
                }
            }
        });

    }

    class DownloadFileFromURL extends AsyncTask<String, Integer, String> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }

        @Override
        protected String doInBackground(String... apkurl) {
            int count;
            int lenghtOfFile = 0;
            InputStream input = null;
            OutputStream fos = null;

            try {
                URL url = new URL(apkurl[0]);
                URLConnection connection = url.openConnection();
                connection.connect();

                lenghtOfFile = connection.getContentLength(); // 파일 크기를 가져옴

                File path = new File(Environment.getExternalStorageDirectory() + croppedFolderName);
                if (! path.exists())
                    path.mkdirs(); // 디렉토리가 없으면 생성
                outputFile = new File(path, outputFileName);
                if (outputFile.exists()) { // 기존 파일 존재시 삭제하고 다운로드
                    outputFile.delete();
                }

                input = new BufferedInputStream(url.openStream());
                fos = new FileOutputStream(outputFile);
                byte data[] = new byte[1024];
                long total = 0;

                while ((count = input.read(data)) != -1) {
                    if (isCancelled()) {
                        input.close();
                        return String.valueOf(-1);
                    }
                    total = total + count;
                    if (lenghtOfFile > 0) { // 파일 총 크기가 0 보다 크면
                        publishProgress((int) (total * 100 / lenghtOfFile));
                    }
                    fos.write(data, 0, count); // 파일에 데이터를 기록
                }

                fos.flush();

            } catch (Exception e) {
                e.printStackTrace();
                Log.e("UpdateAPP", "Update error! " + e.getMessage());
            } finally {
                if (input != null) {
                    try {
                        input.close();
                    }
                    catch(IOException ioex) {
                        //Very bad things just happened... handle it
                    }
                }
                if (fos != null) {
                    try {
                        fos.close();
                    }
                    catch(IOException ioex) {
                        //Very bad things just happened... handle it
                    }
                }
            }
            return null;
        }

        protected void onProgressUpdate(Integer... progress) {
            super.onProgressUpdate(progress);
            // 백그라운드 작업의 진행상태를 표시하기 위해서 호출하는 메소드
        }

        protected void onPostExecute(String result) {
            if (result == null) {
                // 미디어 스캐닝
                MediaScannerConnection.scanFile(getApplicationContext(), new String[]{outputFile.getAbsolutePath()}, null, new MediaScannerConnection.OnScanCompletedListener() {
                    @Override
                    public void onScanCompleted(String s, Uri uri) {
                    }
                });

                String imageUri = outputFile.getAbsolutePath();
                Glide.with(context).load(imageUri).into(imageView);
            } else {
                Toast.makeText(getApplicationContext(), "다운로드 에러", Toast.LENGTH_LONG).show();
            }
        }

        protected void onCancelled() {
            // cancel메소드를 호출하면 자동으로 호출되는 메소드
            progressBar.setProgress(0);
        }
    }

    private void NonSecretApp_Setting() {
        if(Build.VERSION.SDK_INT >= 26)  { // 출처를 알 수 없는 앱 설정 화면 띄우기
            PackageManager packageManager = getPackageManager();
            boolean isTrue = packageManager.canRequestPackageInstalls();

            if (!packageManager.canRequestPackageInstalls()) {
                AlertDialog.Builder b = new AlertDialog.Builder(this, android.R.style.Theme_DeviceDefault_Light_Dialog);
                b.setTitle("알림");
                b.setMessage("보안을 위해 스마트폰 환경설정의 '앱 설치 허용'을 설정해 주시기 바랍니다.설정화면으로 이동하시겠습니까?");
                b.setCancelable(false);
                b.setPositiveButton("설정하기", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
                        intent.setData(Uri.parse("package:" + getPackageName()));
                        startActivity(intent);
                    }
                });

                b.setNegativeButton("건너띄기", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                });
                b.show();
            }
        }
    }

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


테스트에 사용한 파일을 첨부한다.

imagedownload.zip


이미지 CROP 처리 구현을 깔끔하게 하는 방법을 해보고자 하는데 좀 더 노력이 필요한 듯....

728x90
블로그 이미지

Link2Me

,

마시멜로 버전부터는 권한을 일반 권한(Normal Permission)과 위험 권한(Dangerous Permission)으로 나누었으며, 위험 권한의 경우에는 앱이 실행될 때 사용자에게 권한 부여할 것인지 물어보도록 변경되었다.

대표적인 위험권한은 위치, 카메라, 마이크, 연락처, 전화, 문자, 일정, 센서 등이다.


TedPermission 라이브러리를 이용하여 권한체크 하는 방법으로 간단히 이용할 수 있어 편하다.

제작자 블로그 : http://gun0912.tistory.com/


신규 스마트폰은 SDK 버전이 23 이라고 봐도 무방하다.


targetSdkVersion 23 이상으로 설정한 경우
dependencies {
    implementation 'gun0912.ted:tedpermission:2.0.0'
}
추가한다.


사용 예제

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

public class MainActivity extends AppCompatActivity {
    Context context;

    PermissionListener permissionlistener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            initView(); // 권한이 승인되었을 때 실행할 함수
        }

        @Override
        public void onPermissionDenied(ArrayList<String> deniedPermissions) {
            Toast.makeText(MainActivity.this, "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.", 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[] { Manifest.permission.READ_EXTERNAL_STORAGE,
                            Manifest.permission.WRITE_EXTERNAL_STORAGE,
                            Manifest.permission.CAMERA})
                    .check();

        } else {
            initView(); // 권한 승인이 필요없을 때 실행할 함수
        }
    }

    private void initView() {
        imageView = (ImageView) findViewById(R.id.croppedImageView);
        imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {                

            }
        });
    }

}


필요한 권한을 멀티로 지정해주면 된다.

728x90

'안드로이드 > Android 기능' 카테고리의 다른 글

Android – TextView autoLink attribute  (0) 2018.09.21
Android Notification  (0) 2018.08.24
Android View  (1) 2018.03.20
WindowManager 주요 Flag  (0) 2018.02.24
Transparent Activity floating/draggable on Homscreen  (0) 2018.01.30
블로그 이미지

Link2Me

,

Context mContext;
ImageView mImage = (ImageView) findViewById(R.id.croppedImageView);

String imageUri = Item.getPhoto();
if (imageUri.equals("")) { // 이미지가 없을 때
    int resourceId = R.drawable.photo_base;
    Glide.with(mContext).load(resourceId).into(mImage);
} else { // 이미지가 있을 때
    Glide.with(mContext).load(imageUri).into(mImage);
}


imageUri 는 실제 파일이 존재하는 경로, 예) http://www.abc.com/images/abc.jpg


폰의 폴더에 있는 이미지를 View 할 때

String croppedFolderName = "/DCIM/PhotoTemp";
File outputFile = null;
String outputFileName="myprofile.jpg";
private ImageView imageView;

File path = new File(Environment.getExternalStorageDirectory() + croppedFolderName);
if (! path.exists())
    path.mkdirs(); // 디렉토리가 없으면 생성
outputFile = new File(path, outputFileName);

Bitmap photoBitmap = BitmapFactory.decodeFile(outputFile.getAbsolutePath() );
imageView.setImageBitmap(photoBitmap);


Glide 로 View 하면

String imageUri = outputFile.getAbsolutePath();
Glide.with(context).load(imageUri).into(imageView);

728x90
블로그 이미지

Link2Me

,

FloatingActionButton(FAB)
UI 위에 떠 있는 동그라미 모양에 아이콘처럼 되어 사용되는 버튼이다.

build.gradle 추가 사항
dependencies {
    implementation 'com.google.android.material:material:1.1.0'
}

layout 예시
CoordinatorLayout 의 자식 레이아웃에 Floating Action Button 을 추가

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    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">

    <include layout="@layout/view_drawer" />

    <include layout="@layout/content_body" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:backgroundTint="#60CEDC"
        app:srcCompat="@android:drawable/ic_menu_camera" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>


activity 코드
// Floating Action Button을 리스트 뷰에 적용
FloatingActionButton fab = findViewById(R.id.fab);
fab.attachToListView(listView);
// 이벤트 적용
fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {

    }
});

728x90

'안드로이드 > Layout' 카테고리의 다른 글

Dynamic Layouts  (0) 2019.01.06
Android Fragment 기본 예제  (0) 2018.09.11
LinearLayout weight  (0) 2018.03.01
Android ViewFlipper(뷰플리퍼)  (0) 2017.05.02
Fragment 화면 이동  (0) 2017.03.26
블로그 이미지

Link2Me

,

Android Studio 에서 compileSdkVersion 을 바꾸면 아래와 같이 comile Library 버전을 맞춰줘야 에러가 없어진다.

그래서 찾아보기가 만만치 않아서 적어둔다.


compileSdkVersion 22
targetSdkVersion 22
compile 'com.android.support:support-v4:22.0.0'
compile 'com.android.support:appcompat-v7:22.0.0'

compileSdkVersion 23
targetSdkVersion 23
compile 'com.android.support:support-v4:23.0.0'
compile 'com.android.support:appcompat-v7:23.0.0'

compile 'com.android.support:appcompat-v7:23.1.1'

compile 'com.android.support:appcompat-v7:23.4.0'

compileSdkVersion 25
targetSdkVersion 25
compile 'com.android.support:appcompat-v7:25.1.0'

compileSdkVersion 26
targetSdkVersion 26
compile 'com.android.support:appcompat-v7:26.+'

728x90
블로그 이미지

Link2Me

,

https://developer.android.com/studio/build/application-id?hl=ko  을 읽어보면 내용이 잘 나온다.


개발할 때 동일한 앱을 두개 띄우고 서로간에 기능을 확인하고 싶다면

applicationId "com.tistory.link2me.asynchttpjson" 와 같이 applicationId 를 변경하면 또하나의 앱이 설치된다.

즉, 패키지명과 applicationId 를 다르게 하고서 개발 테스트를 하면 좋을 거 같다.


사무실과 집에 있는 PC 환경이 다르지만 src 폴더만 복사해서 코딩하면 되므로 편리하다.


집에 있는 PC build.gradle 및 Android Studio 3.1.3

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.tistory.link2me.asynchttpjson"
        minSdkVersion 19
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support:design:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    implementation 'com.loopj.android:android-async-http:1.4.9'
    implementation 'com.github.bumptech.glide:glide:3.8.0'
    implementation 'com.android.support:recyclerview-v7:26.1.0'
    implementation 'com.android.support:cardview-v7:26.1.0'
} 


사무실 노트북 build.gradle 및 Android Studio 2.3.3

 apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.0"

    defaultConfig {
        applicationId "com.tistory.link2me.asynchttpjson"
        minSdkVersion 19
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

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

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.loopj.android:android-async-http:1.4.9'
    compile 'com.github.bumptech.glide:glide:3.8.0'
    compile 'com.android.support:recyclerview-v7:24.2.0'
    compile 'com.android.support:cardview-v7:24.2.0'
}



조만간에 집에 있는 PC에서 Android Studio 2.3.3 과 3.1.3 을 동시에 이용할 수 있는 환경을 만들어서 테스트를 해볼 생각이다.

노트북에 있는 관련 파일을 모두 복사하고 환경 세팅을 서로 다르게 하면 둘 다 이용이 가능할 거 같다.


컴파일 속도나 인식속도는 Android Studio 3.1.3 이 엄청 빠르다.

그런데 기존 테스트 코드를 Import Moulde 하면 인식을 잘 못하는 경향이 있다. 버그(?)

그리고 Eclipse 코드를 읽어오기가 Android Studio 2.3.3 에서는 잘 되기 때문에 일단 코드를 읽은 다음에 Android Studio 환경에 맞게 수정하는 것이 편하기도 하고 노트북 속도 문제도 있어서 2.3.3 에서 업그레이드를 중지한 상태다.

728x90
블로그 이미지

Link2Me

,

RecyclerView Adapter 를 별도 Adapter 파일로 만들어서 Activity 간 데이터 처리를 해보고 기록해둔다.

그동안 Inner Class 로 하나의 Activity 내에서 처리하는 것만 해봤는데 Activity간 처리하는 것이라 고려할 사항이 좀 된다.


RecyclerView 는 android.support.v7.widget.CardView 와 같이 사용하면 깔끔한 화면을 볼 수 있다.

또한, view 를 두개 이상 선택적으로 보여주는 것도 편하더라.

Intent 로 화면 전환 처리하는 걸 ContentAdapter 에서 처리하는 걸 문제없이 처리하기 위해 구글링으로 여러 자료를 참조하고 완성된 결과를 얻었다.


두 파일간에 Interface 를 통해 처리를 한다.

OnItemClickListene 를 인터페이스라고 하며 class 가 아닌 interface 라는 키워드를 이용하여 작성한다.

onItemClick함수를 인터페이스 함수라고 한다.

public interface OnItemClickListener { // Class 처럼 상속받아올 인터페이스명
    void onItemClick(View v, int position); // 추상 메소드명
}


자바의 인터페이스 개념 이해를 위해서 구글링을 해보니 기본적인 Interface 개념만 나온다.

안드로이드 인터페이스 예제로 검색해야 원하는 걸 얻을 수 있다.

http://yujuwon.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A6%AC%EC%8A%A4%EB%84%88-%EB%A7%8C%EB%93%A4%EA%B8%B0

http://codeasy.tistory.com/2?category=751348


안드로이드 인터페이스 구성요소 : https://kairo96.gitbooks.io/android/content/ch3.1.html 참조


안드로이드 Interface 를 사용한 예제 중에서 https://gist.github.com/riyazMuhammad/1c7b1f9fa3065aa5a46f 를 참조하면 도움된다. 단, mViewHolder.getPosition() 는 테스트해보니 deprecated 되었다고 동작이 되지 않는다.

position 은 getAdapterPosition() 로 넘기면 정상적으로 원하는 결과를 얻을 수 있다.


본 게시글에서는 Content_Item.java, XML 파일은 모두 생략되어 있으니 구현 로직만 참고하면 많은 도움된다.


Custom View 또는 RecyclerView

public class ContentAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>  {
    private static final int TYPE_ONE = 1;
    private static final int TYPE_TWO = 2;
    Context mContext;
    private ArrayList<Content_Item> IvList;
    Content_Item cItem;
    private OnItemClickListener listener; // 이벤트 리스너를 변수로 선언

    public interface OnItemClickListener { // 인터페이스 정의
        void onItemClick(View v, int position);
    }

    public ContentAdapter(Context mContext, ArrayList<Content_Item> items, OnItemClickListener mOnClickListener) {
        this.mContext = mContext;
        IvList = items;
        this.listener = mOnClickListener;
    }

    // determine which layout to use for the row
    @Override
    public int getItemViewType(int position) {
        Content_Item item = IvList.get(position);
        if (item.getIsFolder().equals("1")) {
            return TYPE_ONE;
        } else if (item.getIsFolder().equals("0")) {
            return TYPE_TWO;
        } else {
            return -1;
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 새로운 뷰를 만든다.
        if (viewType == TYPE_ONE) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.group_item, parent, false);
            return new ViewHolder1(view);
        } else if (viewType == TYPE_TWO) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.person_item, parent, false);
            return new ViewHolder2(view);
        } else {
            throw new RuntimeException("The type has to be ONE or TWO");
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        // ListView의 getView 부분을 담당하는 메소드
        switch (holder.getItemViewType()) {
            case TYPE_ONE:
                ((ViewHolder1) holder).bind(IvList.get(position), listener);
                break;
            case TYPE_TWO:
                ((ViewHolder2) holder).bind(IvList.get(position), listener);
                break;
            default:
                break;
        }
    }

    @Override
    public int getItemCount() {
        return IvList.size(); // 데이터 개수 리턴
    }

    public class ViewHolder1 extends RecyclerView.ViewHolder {
        public ImageView mImage;
        public TextView mTitle;

        public ViewHolder1(View itemView) {
            super(itemView);
            // 화면에 표시될 View 로부터 위젯에 대한 참조 획득
            mImage = (ImageView) itemView.findViewById(R.id.cell_image);
            mTitle = (TextView) itemView.findViewById(R.id.cell_text);
        }

        public void bind(Content_Item item, final OnItemClickListener listener) {
            cItem = item;
            if(cItem.getIsFolder().equals("1")){
                // 아이템 내 각 위젯에 데이터 반영
                mImage.setImageResource(R.drawable.group_btn);
                mTitle.setText(cItem.getName());
                itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        listener.onItemClick(view,getAdapterPosition());
                    }
                });
            }
        }

    }

    public class ViewHolder2 extends RecyclerView.ViewHolder {
        public ImageView mImage;
        public TextView mTitle;
        public TextView msubTitle1;
        public TextView msubTitle2;

        public ViewHolder2(View itemView) {
            super(itemView);
            mImage = (ImageView) itemView.findViewById(R.id.cell_image);
            mTitle = (TextView) itemView.findViewById(R.id.cell_text);
            msubTitle1 = (TextView) itemView.findViewById(R.id.cell_text_sub1);
            msubTitle2 = (TextView) itemView.findViewById(R.id.cell_text_sub2);
        }

        public void bind(Content_Item item, final OnItemClickListener listener) {
            cItem = item;
            if(cItem.getIsFolder().equals("0")){
                // 아이템 내 각 위젯에 데이터 반영
                String imageUri = cItem.getPhoto();
                if (imageUri.equals("")) {
                    mImage.setImageBitmap(R.drawable.photo_base);
                } else {
                    Glide.with(itemView.getContext()).load(imageUri).into(mImage);
                }
                mTitle.setText(cItem.getName());
                msubTitle1.setText(cItem.getCode());
                msubTitle2.setText(cItem.getPosition());

                itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        listener.onItemClick(view,getAdapterPosition());
                    }
                });
            }
        }
    }
}

public class RecyclerviewList extends AppCompatActivity implements View.OnClickListener {
    Context context;

    private RecyclerView listView; // 리스트뷰
    private ArrayList<Content_Item> cItemList = new ArrayList<>();
    RecyclerView.Adapter contentListAdapter;
    RecyclerView.LayoutManager layoutManager;

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

        // Adapter에 추가 데이터를 저장하기 위한 ArrayList
        listView = (RecyclerView) findViewById(R.id.recyclerview);
        listView.setHasFixedSize(true);
        // Set Layout Manager
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        listView.setLayoutManager(layoutManager);

        contentListAdapter = new ContentAdapter(getApplicationContext(),cItemList, new ContentAdapter.OnItemClickListener(){

            @Override
            public void onItemClick(View v, int position) {
                if(cItemList.get(position).getIsFolder().equals("1")){
                    Toast.makeText(context, "Item Clicked" + position, Toast.LENGTH_LONG).show();
                } else if(cItemList.get(position).getIsFolder().equals("0")){
                    Intent intent = new Intent(RecyclerviewList.this, StaffView.class);
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
                    intent.putExtra("idx", cItemList.get(position).getIdx());
                    intent.putExtra("title", cItemList.get(position).getName());
                    startActivity(intent);
                }
            }
        }); // Adapter 생성
        listView.setAdapter(contentListAdapter); // 어댑터를 리스트뷰에 세팅

        // 서버에서 데이터 가져오기
        getJSONData();
    }

}


검색으로 찾은 자료들이 대부분 개념만 설명되어 완성된 결과를 얻을 수가 없어 삽질을 한참 했다.

728x90

'안드로이드 > Interface' 카테고리의 다른 글

Android Interface AsyncTask 예제  (0) 2019.11.05
Java Interface Example  (0) 2019.09.05
Java 인터페이스(interface) 개요  (0) 2019.08.20
Android Interface 예제 ★★★  (0) 2018.08.22
Android Interface 예제 1  (1) 2018.08.20
블로그 이미지

Link2Me

,

앱의 데이터가 자동으로 백업될 수 있다는 걸 몰랐다.

출처 : https://m.blog.naver.com/PostView.nhn?blogId=netrance&logNo=221224761858&targetKeyword=&targetRecommendationCode=1


안드로이드 6.0부터는 AndroidManifest.xml 파일에서 application 요소의 android:allowBackup 속성을 자동으로 true로 설정된다.
이로 인해 사용자도 모르게 데이터가 구글 클라우드에 자동으로 백업된다.


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

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

728x90
블로그 이미지

Link2Me

,

recyclerview with multiple view types example 로 검색하면 검색되는 게시글이 https://hashcode.co.kr/questions/561/recyclerview%EC%97%90-%EC%97%AC%EB%9F%AC%EA%B0%9C%EC%9D%98-%EB%B7%B0%ED%83%80%EC%9E%85%EC%9D%84-%EB%84%A3%EC%9D%84-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C%EC%9A%94 로 나온다.

이와 비슷한 답변이 여기저기 검색된다. 나같은 초보한테는 전혀 도움이 안된다.

완성된 예제 코드를 봐야 약간 활용할 수 있게 된다.


https://www.loopwiki.com/ui-ux-design/recyclerview-with-header-and-footer-android-example/


http://www.sunilandroid.com/2016/11/multiple-view-type-in-recyclerview-in.html


https://guides.codepath.com/android/Heterogenous-Layouts-inside-RecyclerView


직접 테스트해본 것은 아니지만 괜찮은 코드로 보인다.


http://www.codexpedia.com/android/android-recyclerview-with-multiple-different-layouts/

이 게시글을 참조하고 나서 원하는 결과를 구현할 수 있었다.


코드를 구현하면서 Type_1, Type_2 가 서로 내용이 다르고, 서버에서 가져올 데이터의 코드도 서로 상이하여 고민이 많았다.

하지만 이런 코드를 검색하면서 ArrayList 는 1개로 통일하여 설계를 하고 Class 를 만들어야 한다는 점이다.

서버에서 가져올 코드에서 최대한 일치할 수 있는 것을 일치하고 없는 칼럼은 공백으로 저장을 하더라도 하나의 규격으로 Class를 만들어서 활용하면 View Type 를 서로 다르게 보여주는 것이 가능하다.


테스트에 사용된 dependencies 는 아래와 같다.

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

    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    implementation 'com.loopj.android:android-async-http:1.4.9' // 서버와의 통신 라이브러리
    implementation 'com.github.bumptech.glide:glide:3.8.0' // 이미지 라이브러리
    implementation 'com.android.support:recyclerview-v7:24.2.0' // ListView 개선 버전
    implementation "com.android.support:cardview-v7:24.2.0"
}  ===> Android Studio 3.1.3


dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.loopj.android:android-async-http:1.4.9' // 서버와의 통신 라이브러리
    compile 'com.github.bumptech.glide:glide:3.8.0' // 이미지 라이브러리
    compile 'com.android.support:recyclerview-v7:24.2.0' // ListView 개선 버전
    compile "com.android.support:cardview-v7:24.2.0"
===> Android Studio 2.3.3


728x90
블로그 이미지

Link2Me

,

인터넷을 검색하여 사용자가 만든 라이브러리를 사용하여 이미지 로딩 처리를 하기도 하는데 이미지 로딩 라이브러리를 사용하는 것이 좋을 거 같아서 테스트를 해보고 있다.


Glide는 안드로이드 이미지 로딩 라이브러리로, 빠르고 효율적인 미디어 관리 오픈소스이고 이미지 로딩 프레임워크다. 미디어 디코딩, 메모리와 디스크 캐싱, 리소스 풀링을 간단하고 쉽게 할 수 있도록 도와준다.


사용법

Glide.with(this).load("http://goo.gl/gEgYUd").into(imageView);

- RecyclerView.Adapter listViewAdapter 에서는 this 대신에 context 사용


테스트 환경

Android Studio 2.3.3 버전 ==> 노트북 스펙 부족으로 최신 3.1.3 으로 업그레이드 중지


compileSdkVersion 26
buildToolsVersion "26.0.0"

defaultConfig {
    applicationId "com.tistory.link2me.asynchttpjson"
    minSdkVersion 19
    targetSdkVersion 26
    versionCode 1
    versionName "1.0"

    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}


dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.loopj.android:android-async-http:1.4.9' // 서버와의 통신 라이브러리
    compile 'com.github.bumptech.glide:glide:3.8.0' // 이미지 라이브러리
    compile 'com.android.support:recyclerview-v7:24.2.0' // ListView 개선 버전
    compile "com.android.support:cardview-v7:24.2.0"
}


https://github.com/bumptech/glide 에 가면 최신버전이 4.7.1 로 나오는데 이걸 사용하면 buildToolsVersion 을 최신으로 사용하라고 나온다. 그래서 4.6.0 으로 해서 컴파일 해봤는데 recyclerview 에서 제대로 처리가 안되는지 에러가 발생하면서 앱이 종료된다. 4.5.0 4.2.0 으로도 시도했으나 실패된다.

glide:3.6.0, 3.7.0, 3.8.0 으로 한 이후에 문제없이 정상동작된다. ==> Android Studio 3.1.3 에서도 정상 동작된다.

자세히 공부하기 싫어서 검색하는 건 포기.....

https://bumptech.github.io/glide/doc/migrating.html


implementation 'com.squareup.picasso:picasso:2.5.2' // 이미지 라이브러리

Picasso 라이브러리를 사용하는데 에러가 발생하여 Glide 라이브러리를 사용해본 건데 아무 버전이나 잘 동작되는건 아닌거 같다.


라이브러리는 버전업이 되면서 오히려 버그가 발생하기도 해서 가능하면 사용하고 싶지 않은데, 코드가 너무 길어지고 메모리 에러 발생 등 고려할 요인들이 너무 많아서 잘 만들어진 라이브러리를 사용하는 것이 속 편하다.


참고하면 도움되는 글

https://d2.naver.com/helloworld/429368


728x90
블로그 이미지

Link2Me

,

CardView를 사용하는 가장 중요한 이유는 깔끔하고 이뻐서다!

안드로이드 CardView 는 support.v7.widget 에 속한 라이브러리로 SDK 21버전부터 CardView가 추가되었다.

Android RecyclerView 를 사용하면서 CardView 를 같이 사용 할 수 있다.

하나의 RecyclerView 내부에 CardView가 들어가 있는 형태라고 보면 된다.


기본적인 설명은 https://www.journaldev.com/10024/android-recyclerview-android-cardview-example-tutorial 사이트를 참조하면 도움된다.


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    implementation 'com.loopj.android:android-async-http:1.4.9' // 서버와의 통신 처리
    implementation 'com.squareup.picasso:picasso:2.5.2' // 이미지 라이브러리
    implementation 'com.android.support:recyclerview-v7:24.2.0' // ListView 개선 버전
    implementation "com.android.support:cardview-v7:24.2.0"
}


android:layout_margin="1dp" 로 했더니 깔끔하지 않아서 android:layout_marginBottom="1dp" 로 처리

cardCornerRadius : 레이아웃에 모서리 반지름 설정

cardBackgroundColor : 카드의 배경색을 설정

contentPadding : 자식 뷰 사이의 내부 간격을 조정

cardElevation : 그림자가 있는 카드를 생성


<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cardview"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="1dp"
    app:cardCornerRadius="1dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="12dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp">

        <ImageView
            android:id="@+id/list_cell_image"
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:layout_gravity="center"
            android:layout_marginLeft="10dp"
            android:src="@drawable/expose_btn_n" />

        <LinearLayout
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginLeft="20dp"
            android:layout_weight="4"
            android:orientation="vertical">

            <TextView
                android:id="@+id/list_cell_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="타이틀"
                android:textSize="20dp"
                android:textStyle="bold" />

        </LinearLayout>
    </LinearLayout>
</android.support.v7.widget.CardView>



API 28 이상에서는

<android.support.v7.widget.CardView 대신에 <androidx.cardview.widget.CardView 로 변경해야 한다.


앱 build.gradle 에서도

implementation 'androidx.cardview:cardview:1.0.0' 를 사용해야 한다.

728x90
블로그 이미지

Link2Me

,

Android Studio 3.0.3 에서 업데이트를 시도하면 fonts.xml 파일이 수정되었다고 업그레이드가 계속 거부되어 포기하고 있다가, 다른 PC에서 새로 Android Studio 3.1.3 을 설치한 폴더를 별도 폴더를 만들어서 실행했더니 내부적으로 코드를 업데이트하더니 잘되던 프로젝트 코드가 전혀 인식도 안되고 동작이 안된다.


지금으로서는 괜히 업그레이드를 했나 싶을 정도로 맨붕 상태다.

한동안 안드로이드 코드는 들여다보지 않다가 시도하려니 그나마 알던 것도 기억 저편에서도 사라졌는지 하나도 생각나지 않는다.


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:23.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
}


2018년까지만 compile 로 dependencies 사용이 가능하고 그 이후에는 무조건 implementation 을 사용해야 한다는 내용이 나온다.


모듈 import 를 해도 기존 버전 파일을 인식하지 못하여 폴더 생성을 자동으로 하지 못한다.

폴더를 그냥 복사해서 넣으면 이미 있다고 나온다.

검색해보니 gradle-wrapper.properties 에서 4.4 로 되어 있는데 아래와 같이 변경하란다.

distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 으로 변경 했더니 에러 발생한다.

시간이 엄청 오래 걸린다. 다시 버전을 4.5 로 변경하고 Sync now 를 눌러서 재빌드 중이다.

4.5로의 변경은 성공이라고 나온다. 하지만 모듈 import 는 실패된다. 실제 폴더에 파일은 복사되어 있다.

Minimum supported Gradle version is 4.4. Current version is 4.3.

그래서 원복으로 4.4 로 변경하고 포기했다.


The specified Android SDK Build Tools version (26.0.2) is ignored, as it is below the minimum supported version (27.0.3) for Android Gradle Plugin 3.1.3.
Android SDK Build Tools 27.0.3 will be used.


신규로 Module를 생성하는 것은 잘된다. 그래서 모듈을 별도 생성하면서 처리하는 중이다.





728x90
블로그 이미지

Link2Me

,